@prismshadow/penguin-server 0.2.10 → 0.2.13
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/dist/api/types.d.ts +1014 -12
- package/dist/api/types.js +7 -0
- package/dist/auth-token.js +6 -4
- package/dist/auth-token.js.map +1 -1
- package/dist/{chunk-Q65VPAQS.js → chunk-3I2XOPJB.js} +102 -55
- package/dist/chunk-3I2XOPJB.js.map +1 -0
- package/dist/{chunk-ADZJYDNC.js → chunk-A3QANMU5.js} +293 -3
- package/dist/chunk-A3QANMU5.js.map +1 -0
- package/dist/chunk-B5IP37ZQ.js +56 -0
- package/dist/chunk-L2H3K4DT.js +7 -0
- package/dist/chunk-L2H3K4DT.js.map +1 -0
- package/dist/chunk-LPPVGHYD.js +83 -0
- package/dist/chunk-LPPVGHYD.js.map +1 -0
- package/dist/{chunk-TTGGJDNA.js → chunk-S3BI6R7I.js} +2 -2
- package/dist/{chunk-HOS3W3UP.js → chunk-TOOTEEM4.js} +50 -8
- package/dist/chunk-TOOTEEM4.js.map +1 -0
- package/dist/hmr/manifest.d.ts +1 -77
- package/dist/hmr/manifest.js +6 -1
- package/dist/index.js +50757 -20174
- package/dist/index.js.map +1 -1
- package/dist/initial-password.js +1 -0
- package/dist/lock.js +1 -0
- package/dist/machine-status.js +2 -1
- package/dist/machine-status.js.map +1 -1
- package/dist/plugin/index.d.ts +2595 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/reset-admin-password.js +7 -5
- package/dist/reset-admin-password.js.map +1 -1
- package/dist/secret-file.js +2 -0
- package/dist/secret-file.js.map +1 -1
- package/dist/version-report.js +3 -2
- package/package.json +11 -9
- package/web-dist/assets/{addon-clipboard-1cARCaK9.js → addon-clipboard-DrZY90Ks.js} +1 -1
- package/web-dist/assets/{addon-fit-BWdzl8Er.js → addon-fit-CtEAdAax.js} +1 -1
- package/web-dist/assets/{addon-web-links-DjXuvn4Z.js → addon-web-links-DJL13cYY.js} +1 -1
- package/web-dist/assets/c-BG-tZi7o.js +1 -0
- package/web-dist/assets/cpp-CaeG3uDQ.js +1 -0
- package/web-dist/assets/css-Br67jR8j.js +1 -0
- package/web-dist/assets/diff-jW5kmiru.js +1 -0
- package/web-dist/assets/github-dark-BQV5ugrc.js +1 -0
- package/web-dist/assets/github-light-Rcb2q3L5.js +1 -0
- package/web-dist/assets/go-B7VTfpnm.js +1 -0
- package/web-dist/assets/highlighter-CUb4XKtQ.js +2 -0
- package/web-dist/assets/{highlighter-Cu43iYr8.js → highlighter-core-CrBdruEx.js} +2 -2
- package/web-dist/assets/highlighter.worker-CL6f9Fzw.js +152 -0
- package/web-dist/assets/html-BW_6VhB7.js +1 -0
- package/web-dist/assets/index-CmfumtXy.js +570 -0
- package/web-dist/assets/index-Ywc2uM5u.css +1 -0
- package/web-dist/assets/ini-Dix3pTmg.js +1 -0
- package/web-dist/assets/java-BTDmaRNO.js +1 -0
- package/web-dist/assets/javascript-WyGyM1wV.js +1 -0
- package/web-dist/assets/json-B8JryHcM.js +1 -0
- package/web-dist/assets/jsx-8cLv2Six.js +1 -0
- package/web-dist/assets/log-CcaF_xNv.js +1 -0
- package/web-dist/assets/markdown-DX-TNHnc.js +1 -0
- package/web-dist/assets/php-CNeTOyEV.js +1 -0
- package/web-dist/assets/python-B6Ihcp-E.js +1 -0
- package/web-dist/assets/ruby-CYiNE6ps.js +1 -0
- package/web-dist/assets/rust-CxuBE0J8.js +1 -0
- package/web-dist/assets/shellscript-DgJyDXW6.js +1 -0
- package/web-dist/assets/sql-Dq2MiB0T.js +1 -0
- package/web-dist/assets/toml-Ms459DBw.js +1 -0
- package/web-dist/assets/tsx-D6116Ycy.js +1 -0
- package/web-dist/assets/typescript-BqvgUolh.js +1 -0
- package/web-dist/assets/xml-BkAJuBCc.js +1 -0
- package/web-dist/assets/{xterm-DKcY6q-I.js → xterm-B2oS8Mg8.js} +1 -1
- package/web-dist/assets/yaml-Bopit2L3.js +1 -0
- package/web-dist/index.html +2 -2
- package/dist/chunk-4SXGU7RO.js +0 -50
- package/dist/chunk-4SXGU7RO.js.map +0 -1
- package/dist/chunk-ADZJYDNC.js.map +0 -1
- package/dist/chunk-HOS3W3UP.js.map +0 -1
- package/dist/chunk-Q65VPAQS.js.map +0 -1
- package/dist/extension/index.d.ts +0 -212
- package/web-dist/assets/index-Bm9n7ZV-.js +0 -457
- package/web-dist/assets/index-DR4PrRzS.css +0 -1
- /package/dist/{extension/index.js.map → chunk-B5IP37ZQ.js.map} +0 -0
- /package/dist/{chunk-TTGGJDNA.js.map → chunk-S3BI6R7I.js.map} +0 -0
- /package/dist/{extension → plugin}/index.js +0 -0
package/dist/api/types.js
CHANGED
package/dist/auth-token.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AuthSessionsRepo,
|
|
3
3
|
UsersRepo
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-TOOTEEM4.js";
|
|
5
5
|
import {
|
|
6
6
|
openExistingDatabase
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-A3QANMU5.js";
|
|
8
|
+
import "./chunk-B5IP37ZQ.js";
|
|
8
9
|
|
|
9
10
|
// src/auth-token.ts
|
|
10
11
|
import path from "path";
|
|
11
12
|
import fs from "fs";
|
|
13
|
+
import { wire } from "@prismshadow/penguin-core/kernel";
|
|
12
14
|
var CLI_TOKEN_TTL_MS = 60 * 6e4;
|
|
13
15
|
var CLI_TOKEN_MAX_TTL_MS = 30 * 24 * 60 * 6e4;
|
|
14
16
|
function explain(err, dbPath) {
|
|
@@ -28,10 +30,10 @@ function mintApiToken(root, opts = {}) {
|
|
|
28
30
|
let db = null;
|
|
29
31
|
try {
|
|
30
32
|
db = openExistingDatabase(dbPath);
|
|
31
|
-
if (
|
|
33
|
+
if (wire(UsersRepo, { db }).findById(userId) === null) {
|
|
32
34
|
return { outcome: "failed", detail: `no such account: ${userId}` };
|
|
33
35
|
}
|
|
34
|
-
const { token, expiresAt } =
|
|
36
|
+
const { token, expiresAt } = wire(AuthSessionsRepo, { db }).issue({
|
|
35
37
|
userId,
|
|
36
38
|
via: "cli",
|
|
37
39
|
now: opts.now ?? /* @__PURE__ */ new Date(),
|
package/dist/auth-token.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auth-token.ts"],"sourcesContent":["/**\n * Mints an API session by inserting a row into the root's `web.db` — no running server needed.\n * Authorized by being able to read and write that root, which already holds every credential\n * the token could reach. On a multi-user box that means the OS account running the server;\n * everyone else signs in with `penguin auth login --server <url>`.\n */\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport { openExistingDatabase } from \"./db/database.js\";\nimport { AuthSessionsRepo } from \"./db/repos/auth-sessions.js\";\nimport { UsersRepo } from \"./db/repos/users.js\";\n\n/** An hour: long enough for a controller to finish, short enough to forget. */\nexport const CLI_TOKEN_TTL_MS = 60 * 60_000;\n\n/**\n * Ceiling on a caller-supplied `--ttl-seconds`, matching the default session TTL (config.ts):\n * a token that lives in a file must not outlive an ordinary browser session.\n */\nexport const CLI_TOKEN_MAX_TTL_MS = 30 * 24 * 60 * 60_000;\n\nexport type MintTokenResult =\n | { outcome: \"minted\"; token: string; userId: string; expiresAt: string }\n /** No web.db on this root — the server has never run here, so there is no account to mint for. */\n | { outcome: \"no_server\" }\n | { outcome: \"failed\"; detail: string };\n\n/**\n * Turns a failure into something the person at the terminal can act on. Both cases are\n * ordinary situations, not crashes: a database owned by another OS account, and one written\n * by an older release (minting deliberately does not migrate — see openExistingDatabase).\n */\nfunction explain(err: unknown, dbPath: string): string {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes(\"no column named via\")) {\n return `${dbPath} predates this release — start the server once (\\`penguin web\\`) to upgrade it`;\n }\n if (message.includes(\"unable to open database\")) {\n return (\n `cannot open ${dbPath} — minting needs the data root's own OS account; ` +\n \"otherwise sign in with `penguin auth login --server <url>`\"\n );\n }\n return message;\n}\n\n/** Issues an API session token for the data root at `root`. */\nexport function mintApiToken(\n root: string,\n opts: { userId?: string; ttlMs?: number; dbPath?: string; now?: Date } = {},\n): MintTokenResult {\n const userId = opts.userId ?? \"admin\";\n const dbPath = opts.dbPath ?? path.join(root, \"web.db\");\n // Existence check first: openExistingDatabase would otherwise create an empty file, and a\n // root with no web.db has no account to mint for.\n if (!fs.existsSync(dbPath)) return { outcome: \"no_server\" };\n\n let db: ReturnType<typeof openExistingDatabase> | null = null;\n try {\n db = openExistingDatabase(dbPath);\n if (
|
|
1
|
+
{"version":3,"sources":["../src/auth-token.ts"],"sourcesContent":["/**\n * Mints an API session by inserting a row into the root's `web.db` — no running server needed.\n * Authorized by being able to read and write that root, which already holds every credential\n * the token could reach. On a multi-user box that means the OS account running the server;\n * everyone else signs in with `penguin auth login --server <url>`.\n */\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport { openExistingDatabase } from \"./db/database.js\";\nimport { AuthSessionsRepo } from \"./db/repos/auth-sessions.js\";\nimport { UsersRepo } from \"./db/repos/users.js\";\nimport { wire } from \"@prismshadow/penguin-core/kernel\";\n\n/** An hour: long enough for a controller to finish, short enough to forget. */\nexport const CLI_TOKEN_TTL_MS = 60 * 60_000;\n\n/**\n * Ceiling on a caller-supplied `--ttl-seconds`, matching the default session TTL (config.ts):\n * a token that lives in a file must not outlive an ordinary browser session.\n */\nexport const CLI_TOKEN_MAX_TTL_MS = 30 * 24 * 60 * 60_000;\n\nexport type MintTokenResult =\n | { outcome: \"minted\"; token: string; userId: string; expiresAt: string }\n /** No web.db on this root — the server has never run here, so there is no account to mint for. */\n | { outcome: \"no_server\" }\n | { outcome: \"failed\"; detail: string };\n\n/**\n * Turns a failure into something the person at the terminal can act on. Both cases are\n * ordinary situations, not crashes: a database owned by another OS account, and one written\n * by an older release (minting deliberately does not migrate — see openExistingDatabase).\n */\nfunction explain(err: unknown, dbPath: string): string {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes(\"no column named via\")) {\n return `${dbPath} predates this release — start the server once (\\`penguin web\\`) to upgrade it`;\n }\n if (message.includes(\"unable to open database\")) {\n return (\n `cannot open ${dbPath} — minting needs the data root's own OS account; ` +\n \"otherwise sign in with `penguin auth login --server <url>`\"\n );\n }\n return message;\n}\n\n/** Issues an API session token for the data root at `root`. */\nexport function mintApiToken(\n root: string,\n opts: { userId?: string; ttlMs?: number; dbPath?: string; now?: Date } = {},\n): MintTokenResult {\n const userId = opts.userId ?? \"admin\";\n const dbPath = opts.dbPath ?? path.join(root, \"web.db\");\n // Existence check first: openExistingDatabase would otherwise create an empty file, and a\n // root with no web.db has no account to mint for.\n if (!fs.existsSync(dbPath)) return { outcome: \"no_server\" };\n\n let db: ReturnType<typeof openExistingDatabase> | null = null;\n try {\n db = openExistingDatabase(dbPath);\n if (wire(UsersRepo, { db: db }).findById(userId) === null) {\n return { outcome: \"failed\", detail: `no such account: ${userId}` };\n }\n // The repo is the single minting point (token shape, hash, TTL ceiling), so a CLI-minted\n // row cannot drift from one the server issues itself.\n const { token, expiresAt } = wire(AuthSessionsRepo, { db: db }).issue({\n userId,\n via: \"cli\",\n now: opts.now ?? new Date(),\n ttlMs: opts.ttlMs ?? CLI_TOKEN_TTL_MS,\n maxTtlMs: CLI_TOKEN_MAX_TTL_MS,\n });\n return { outcome: \"minted\", token, userId, expiresAt };\n } catch (err) {\n return { outcome: \"failed\", detail: explain(err, dbPath) };\n } finally {\n db?.close();\n }\n}\n"],"mappings":";;;;;;;;;;AAMA,OAAO,UAAU;AACjB,OAAO,QAAQ;AAIf,SAAS,YAAY;AAGd,IAAM,mBAAmB,KAAK;AAM9B,IAAM,uBAAuB,KAAK,KAAK,KAAK;AAanD,SAAS,QAAQ,KAAc,QAAwB;AACrD,QAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,MAAI,QAAQ,SAAS,qBAAqB,GAAG;AAC3C,WAAO,GAAG,MAAM;AAAA,EAClB;AACA,MAAI,QAAQ,SAAS,yBAAyB,GAAG;AAC/C,WACE,eAAe,MAAM;AAAA,EAGzB;AACA,SAAO;AACT;AAGO,SAAS,aACd,MACA,OAAyE,CAAC,GACzD;AACjB,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,SAAS,KAAK,UAAU,KAAK,KAAK,MAAM,QAAQ;AAGtD,MAAI,CAAC,GAAG,WAAW,MAAM,EAAG,QAAO,EAAE,SAAS,YAAY;AAE1D,MAAI,KAAqD;AACzD,MAAI;AACF,SAAK,qBAAqB,MAAM;AAChC,QAAI,KAAK,WAAW,EAAE,GAAO,CAAC,EAAE,SAAS,MAAM,MAAM,MAAM;AACzD,aAAO,EAAE,SAAS,UAAU,QAAQ,oBAAoB,MAAM,GAAG;AAAA,IACnE;AAGA,UAAM,EAAE,OAAO,UAAU,IAAI,KAAK,kBAAkB,EAAE,GAAO,CAAC,EAAE,MAAM;AAAA,MACpE;AAAA,MACA,KAAK;AAAA,MACL,KAAK,KAAK,OAAO,oBAAI,KAAK;AAAA,MAC1B,OAAO,KAAK,SAAS;AAAA,MACrB,UAAU;AAAA,IACZ,CAAC;AACD,WAAO,EAAE,SAAS,UAAU,OAAO,QAAQ,UAAU;AAAA,EACvD,SAAS,KAAK;AACZ,WAAO,EAAE,SAAS,UAAU,QAAQ,QAAQ,KAAK,MAAM,EAAE;AAAA,EAC3D,UAAE;AACA,QAAI,MAAM;AAAA,EACZ;AACF;","names":[]}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sessionTokenHash
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-TOOTEEM4.js";
|
|
4
|
+
import {
|
|
5
|
+
__decorateElement,
|
|
6
|
+
__decoratorStart,
|
|
7
|
+
__publicField,
|
|
8
|
+
__runInitializers
|
|
9
|
+
} from "./chunk-B5IP37ZQ.js";
|
|
4
10
|
|
|
5
11
|
// src/auth/password.ts
|
|
12
|
+
import { Component, Interface } from "@prismshadow/penguin-core/kernel";
|
|
6
13
|
import { randomBytes, scrypt, timingSafeEqual } from "crypto";
|
|
7
14
|
var SCRYPT_COST = 16384;
|
|
8
15
|
var SCRYPT_R = 8;
|
|
@@ -53,6 +60,18 @@ async function verifyPassword(password, stored) {
|
|
|
53
60
|
}
|
|
54
61
|
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
55
62
|
}
|
|
63
|
+
var PasswordHasher = class extends Interface() {
|
|
64
|
+
};
|
|
65
|
+
var _ScryptHasher_decorators, _init;
|
|
66
|
+
_ScryptHasher_decorators = [Component()];
|
|
67
|
+
var ScryptHasher = class {
|
|
68
|
+
hash(password) {
|
|
69
|
+
return hashPassword(password, SCRYPT_COST);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
_init = __decoratorStart(null);
|
|
73
|
+
ScryptHasher = __decorateElement(_init, 0, "ScryptHasher", _ScryptHasher_decorators, ScryptHasher);
|
|
74
|
+
__runInitializers(_init, 1, ScryptHasher);
|
|
56
75
|
|
|
57
76
|
// src/auth/service.ts
|
|
58
77
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
@@ -117,6 +136,7 @@ function handleError(err, c) {
|
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
// src/auth/service.ts
|
|
139
|
+
import { Component as Component2, Use, Interface as Interface2 } from "@prismshadow/penguin-core/kernel";
|
|
120
140
|
var MIN_PASSWORD_LENGTH = 8;
|
|
121
141
|
var ADMIN_USER_ID = "admin";
|
|
122
142
|
var LOGIN_FREE_ATTEMPTS = 5;
|
|
@@ -132,22 +152,38 @@ function toUserInfo(row) {
|
|
|
132
152
|
userId: row.userId,
|
|
133
153
|
isAdmin: row.isAdmin,
|
|
134
154
|
passwordIsInitial: row.passwordIsInitial,
|
|
155
|
+
...row.displayName !== null ? { displayName: row.displayName } : {},
|
|
156
|
+
...row.avatar !== null ? { avatar: row.avatar } : {},
|
|
135
157
|
createdAt: row.createdAt
|
|
136
158
|
};
|
|
137
159
|
}
|
|
160
|
+
var _hasher_dec, _clock_dec, _config_dec, _provisioner_dec, _state_dec, _authSessions_dec, _users_dec, _AuthService_decorators, _init2;
|
|
161
|
+
_AuthService_decorators = [Component2()], _users_dec = [Use()], _authSessions_dec = [Use()], _state_dec = [Use()], _provisioner_dec = [Use()], _config_dec = [Use()], _clock_dec = [Use()], _hasher_dec = [Use()];
|
|
138
162
|
var AuthService = class {
|
|
139
|
-
constructor(
|
|
140
|
-
this
|
|
141
|
-
this
|
|
142
|
-
this
|
|
143
|
-
this
|
|
163
|
+
constructor() {
|
|
164
|
+
__publicField(this, "users", __runInitializers(_init2, 8, this)), __runInitializers(_init2, 11, this);
|
|
165
|
+
__publicField(this, "authSessions", __runInitializers(_init2, 12, this)), __runInitializers(_init2, 15, this);
|
|
166
|
+
__publicField(this, "state", __runInitializers(_init2, 16, this)), __runInitializers(_init2, 19, this);
|
|
167
|
+
__publicField(this, "provisioner", __runInitializers(_init2, 20, this)), __runInitializers(_init2, 23, this);
|
|
168
|
+
__publicField(this, "config", __runInitializers(_init2, 24, this)), __runInitializers(_init2, 27, this);
|
|
169
|
+
__publicField(this, "clock", __runInitializers(_init2, 28, this)), __runInitializers(_init2, 31, this);
|
|
170
|
+
__publicField(this, "hasher", __runInitializers(_init2, 32, this)), __runInitializers(_init2, 35, this);
|
|
171
|
+
/** Consecutive login failures per userId (see the throttling comment on the constants). */
|
|
172
|
+
__publicField(this, "loginFailures", /* @__PURE__ */ new Map());
|
|
144
173
|
}
|
|
145
|
-
deps;
|
|
146
|
-
now;
|
|
147
|
-
hashCost;
|
|
148
174
|
/** Session lifetime, for the cookie that must expire WITH the session, not before it. */
|
|
149
175
|
get sessionTtlMs() {
|
|
150
|
-
return this.
|
|
176
|
+
return this.config.authSessionTtlMs;
|
|
177
|
+
}
|
|
178
|
+
get sessionRenewMs() {
|
|
179
|
+
return this.config.authSessionRenewMs;
|
|
180
|
+
}
|
|
181
|
+
/** Fixed initial password for the seeded admin; null generates a random one at seed time. */
|
|
182
|
+
get seedAdminPassword() {
|
|
183
|
+
return this.config.seedAdminPassword;
|
|
184
|
+
}
|
|
185
|
+
setup() {
|
|
186
|
+
this.authSessions.deleteExpired(this.clock.now().toISOString());
|
|
151
187
|
}
|
|
152
188
|
/**
|
|
153
189
|
* Null once the server is claimed, so the only setup session that exists is a printed one.
|
|
@@ -156,11 +192,11 @@ var AuthService = class {
|
|
|
156
192
|
*/
|
|
157
193
|
mintFirstLogin() {
|
|
158
194
|
if (!this.adminPasswordIsInitial()) return null;
|
|
159
|
-
if (this.
|
|
160
|
-
this.
|
|
195
|
+
if (this.state.firstLoginToken !== null && this.authenticateWithMeta(this.state.firstLoginToken) === null) {
|
|
196
|
+
this.state.firstLoginToken = null;
|
|
161
197
|
}
|
|
162
|
-
this.
|
|
163
|
-
return this.
|
|
198
|
+
this.state.firstLoginToken ??= this.issueSession(ADMIN_USER_ID, "setup");
|
|
199
|
+
return this.state.firstLoginToken;
|
|
164
200
|
}
|
|
165
201
|
/**
|
|
166
202
|
* For the startup notice's pinned-seed gate (index.ts): a pin usually means the operator
|
|
@@ -168,7 +204,7 @@ var AuthService = class {
|
|
|
168
204
|
* that or the rescue link stays suppressed.
|
|
169
205
|
*/
|
|
170
206
|
async adminPasswordIs(password) {
|
|
171
|
-
const row = this.
|
|
207
|
+
const row = this.users.findById(ADMIN_USER_ID);
|
|
172
208
|
return row !== null && await verifyPassword(password, row.passwordHash);
|
|
173
209
|
}
|
|
174
210
|
/**
|
|
@@ -177,8 +213,8 @@ var AuthService = class {
|
|
|
177
213
|
* the server retries on next startup.
|
|
178
214
|
*/
|
|
179
215
|
async seedAdmin() {
|
|
180
|
-
if (this.
|
|
181
|
-
const password = this.
|
|
216
|
+
if (this.users.count() > 0) return;
|
|
217
|
+
const password = this.seedAdminPassword ?? generateInitialAdminPassword();
|
|
182
218
|
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
183
219
|
throw new Error(
|
|
184
220
|
`PENGUIN_SEED_ADMIN_PASSWORD must be at least ${MIN_PASSWORD_LENGTH} characters.`
|
|
@@ -186,16 +222,18 @@ var AuthService = class {
|
|
|
186
222
|
}
|
|
187
223
|
const user = {
|
|
188
224
|
userId: ADMIN_USER_ID,
|
|
189
|
-
passwordHash: await
|
|
225
|
+
passwordHash: await this.hasher.hash(password),
|
|
190
226
|
isAdmin: true,
|
|
191
227
|
passwordIsInitial: true,
|
|
192
|
-
|
|
228
|
+
displayName: null,
|
|
229
|
+
avatar: null,
|
|
230
|
+
createdAt: this.clock.now().toISOString()
|
|
193
231
|
};
|
|
194
|
-
this.
|
|
232
|
+
this.users.insert(user);
|
|
195
233
|
try {
|
|
196
|
-
await this.
|
|
234
|
+
await this.provisioner.provisionInitialProject(user, true);
|
|
197
235
|
} catch (err) {
|
|
198
|
-
this.
|
|
236
|
+
this.users.delete(user.userId);
|
|
199
237
|
throw err;
|
|
200
238
|
}
|
|
201
239
|
}
|
|
@@ -205,16 +243,14 @@ var AuthService = class {
|
|
|
205
243
|
* because a prefetching browser would burn it before its reader clicked.
|
|
206
244
|
*/
|
|
207
245
|
redeemFirstLogin(given) {
|
|
208
|
-
const expected = this.
|
|
246
|
+
const expected = this.state.firstLoginToken;
|
|
209
247
|
if (expected === null || given === "" || !tokensEqual(given, expected)) return null;
|
|
210
248
|
return this.authenticateWithMeta(expected) === null ? null : expected;
|
|
211
249
|
}
|
|
212
250
|
/** Whether the built-in admin still runs on its initial password (gates the first-login link). */
|
|
213
251
|
adminPasswordIsInitial() {
|
|
214
|
-
return this.
|
|
252
|
+
return this.users.findById(ADMIN_USER_ID)?.passwordIsInitial === true;
|
|
215
253
|
}
|
|
216
|
-
/** Consecutive login failures per userId (see the throttling comment on the constants). */
|
|
217
|
-
loginFailures = /* @__PURE__ */ new Map();
|
|
218
254
|
/** The wait imposed after `failures` consecutive failures (0 while within the free attempts). */
|
|
219
255
|
loginDelayMs(failures) {
|
|
220
256
|
const excess = failures - LOGIN_FREE_ATTEMPTS;
|
|
@@ -222,7 +258,7 @@ var AuthService = class {
|
|
|
222
258
|
return Math.min(LOGIN_BACKOFF_START_MS * 2 ** (excess - 1), LOGIN_BACKOFF_CAP_MS);
|
|
223
259
|
}
|
|
224
260
|
async login(userId, password) {
|
|
225
|
-
const nowMs = this.now().getTime();
|
|
261
|
+
const nowMs = this.clock.now().getTime();
|
|
226
262
|
for (const [key, entry] of this.loginFailures) {
|
|
227
263
|
if (nowMs - entry.lastFailureAt > LOGIN_FAILURE_IDLE_MS) this.loginFailures.delete(key);
|
|
228
264
|
}
|
|
@@ -237,17 +273,17 @@ var AuthService = class {
|
|
|
237
273
|
);
|
|
238
274
|
}
|
|
239
275
|
}
|
|
240
|
-
const row = this.
|
|
276
|
+
const row = this.users.findById(userId);
|
|
241
277
|
const ok = row !== null && await verifyPassword(password, row.passwordHash);
|
|
242
278
|
if (!row || !ok) {
|
|
243
279
|
this.loginFailures.set(userId, {
|
|
244
280
|
failures: (failed?.failures ?? 0) + 1,
|
|
245
|
-
lastFailureAt: this.now().getTime()
|
|
281
|
+
lastFailureAt: this.clock.now().getTime()
|
|
246
282
|
});
|
|
247
283
|
throw new HttpError(401, "invalid_credentials", "Incorrect username or password.");
|
|
248
284
|
}
|
|
249
285
|
this.loginFailures.delete(userId);
|
|
250
|
-
this.
|
|
286
|
+
this.authSessions.deleteExpired(this.clock.now().toISOString());
|
|
251
287
|
return { user: toUserInfo(row), token: this.issueSession(row.userId, "password") };
|
|
252
288
|
}
|
|
253
289
|
/**
|
|
@@ -256,7 +292,7 @@ var AuthService = class {
|
|
|
256
292
|
* broken deployment (seeding runs before the route exists).
|
|
257
293
|
*/
|
|
258
294
|
loginDesktop() {
|
|
259
|
-
const row = this.
|
|
295
|
+
const row = this.users.findById(ADMIN_USER_ID);
|
|
260
296
|
if (!row) {
|
|
261
297
|
throw new HttpError(500, "internal", "Built-in admin has not been seeded.");
|
|
262
298
|
}
|
|
@@ -264,7 +300,7 @@ var AuthService = class {
|
|
|
264
300
|
}
|
|
265
301
|
/** Self password change (user settings): validates the old password first; the current session stays valid. */
|
|
266
302
|
async changePassword(userId, oldPassword, newPassword) {
|
|
267
|
-
const row = this.
|
|
303
|
+
const row = this.users.findById(userId);
|
|
268
304
|
if (!row || !await verifyPassword(oldPassword, row.passwordHash)) {
|
|
269
305
|
throw new HttpError(400, "password_mismatch", "Current password is incorrect.");
|
|
270
306
|
}
|
|
@@ -282,16 +318,16 @@ var AuthService = class {
|
|
|
282
318
|
if (newPassword.length < MIN_PASSWORD_LENGTH) {
|
|
283
319
|
throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
|
|
284
320
|
}
|
|
285
|
-
this.
|
|
321
|
+
this.users.updatePassword(userId, await this.hasher.hash(newPassword), false);
|
|
286
322
|
this.loginFailures.delete(userId);
|
|
287
323
|
if (userId === ADMIN_USER_ID) {
|
|
288
|
-
this.
|
|
289
|
-
this.
|
|
324
|
+
this.authSessions.deleteByUserAndVia(userId, "setup");
|
|
325
|
+
this.state.firstLoginToken = null;
|
|
290
326
|
}
|
|
291
327
|
}
|
|
292
328
|
/** Ends a session: the row is the session, so logout deletes it. Unknown token is a no-op. */
|
|
293
329
|
logout(token) {
|
|
294
|
-
this.
|
|
330
|
+
this.authSessions.delete(sessionTokenHash(token));
|
|
295
331
|
}
|
|
296
332
|
/**
|
|
297
333
|
* The current boot's local API token (what control-env injection hands to tool
|
|
@@ -300,7 +336,7 @@ var AuthService = class {
|
|
|
300
336
|
* outlive the App a push replaces (auth/runtime-state.ts).
|
|
301
337
|
*/
|
|
302
338
|
localApiToken() {
|
|
303
|
-
return this.
|
|
339
|
+
return this.state.apiToken;
|
|
304
340
|
}
|
|
305
341
|
/**
|
|
306
342
|
* Validates a Bearer token against the boot's local API token (constant-time compare):
|
|
@@ -309,10 +345,10 @@ var AuthService = class {
|
|
|
309
345
|
* null on mismatch, when no token was minted, or when the admin row is missing.
|
|
310
346
|
*/
|
|
311
347
|
authenticateApiToken(token) {
|
|
312
|
-
const apiToken = this.
|
|
348
|
+
const apiToken = this.state.apiToken;
|
|
313
349
|
if (apiToken === null || token.length === 0) return null;
|
|
314
350
|
if (!tokensEqual(token, apiToken)) return null;
|
|
315
|
-
const user = this.
|
|
351
|
+
const user = this.users.findById(ADMIN_USER_ID);
|
|
316
352
|
return user === null ? null : { user, via: "token" };
|
|
317
353
|
}
|
|
318
354
|
/**
|
|
@@ -323,37 +359,46 @@ var AuthService = class {
|
|
|
323
359
|
*/
|
|
324
360
|
authenticateWithMeta(token) {
|
|
325
361
|
const tokenHash = sessionTokenHash(token);
|
|
326
|
-
const session = this.
|
|
362
|
+
const session = this.authSessions.findByTokenHash(tokenHash);
|
|
327
363
|
if (!session) return null;
|
|
328
|
-
const now = this.now();
|
|
364
|
+
const now = this.clock.now();
|
|
329
365
|
const expiresAt = Date.parse(session.expiresAt);
|
|
330
366
|
if (expiresAt <= now.getTime()) {
|
|
331
|
-
this.
|
|
367
|
+
this.authSessions.delete(tokenHash);
|
|
332
368
|
return null;
|
|
333
369
|
}
|
|
334
|
-
const user = this.
|
|
370
|
+
const user = this.users.findById(session.userId);
|
|
335
371
|
if (!user) return null;
|
|
336
|
-
const renewable = expiresAt - Date.parse(session.createdAt) >= this.
|
|
372
|
+
const renewable = expiresAt - Date.parse(session.createdAt) >= this.sessionRenewMs;
|
|
337
373
|
let renewed = false;
|
|
338
|
-
if (renewable && expiresAt - now.getTime() < this.
|
|
339
|
-
this.
|
|
340
|
-
tokenHash,
|
|
341
|
-
new Date(now.getTime() + this.deps.sessionTtlMs).toISOString()
|
|
342
|
-
);
|
|
374
|
+
if (renewable && expiresAt - now.getTime() < this.sessionRenewMs) {
|
|
375
|
+
this.authSessions.touch(tokenHash, new Date(now.getTime() + this.sessionTtlMs).toISOString());
|
|
343
376
|
renewed = true;
|
|
344
377
|
}
|
|
345
378
|
const via = session.via === "desktop" ? "desktop" : session.via === "setup" ? "setup" : "password";
|
|
346
379
|
return { user, via, renewed };
|
|
347
380
|
}
|
|
348
381
|
issueSession(userId, via) {
|
|
349
|
-
return this.
|
|
382
|
+
return this.authSessions.issue({
|
|
350
383
|
userId,
|
|
351
384
|
via,
|
|
352
|
-
now: this.now(),
|
|
353
|
-
ttlMs: this.
|
|
385
|
+
now: this.clock.now(),
|
|
386
|
+
ttlMs: this.sessionTtlMs
|
|
354
387
|
}).token;
|
|
355
388
|
}
|
|
356
389
|
};
|
|
390
|
+
_init2 = __decoratorStart(null);
|
|
391
|
+
__decorateElement(_init2, 5, "users", _users_dec, AuthService);
|
|
392
|
+
__decorateElement(_init2, 5, "authSessions", _authSessions_dec, AuthService);
|
|
393
|
+
__decorateElement(_init2, 5, "state", _state_dec, AuthService);
|
|
394
|
+
__decorateElement(_init2, 5, "provisioner", _provisioner_dec, AuthService);
|
|
395
|
+
__decorateElement(_init2, 5, "config", _config_dec, AuthService);
|
|
396
|
+
__decorateElement(_init2, 5, "clock", _clock_dec, AuthService);
|
|
397
|
+
__decorateElement(_init2, 5, "hasher", _hasher_dec, AuthService);
|
|
398
|
+
AuthService = __decorateElement(_init2, 0, "AuthService", _AuthService_decorators, AuthService);
|
|
399
|
+
__runInitializers(_init2, 1, AuthService);
|
|
400
|
+
var InitialProjectProvisioner = class extends Interface2() {
|
|
401
|
+
};
|
|
357
402
|
|
|
358
403
|
export {
|
|
359
404
|
HttpError,
|
|
@@ -363,12 +408,14 @@ export {
|
|
|
363
408
|
handleError,
|
|
364
409
|
mintApiToken,
|
|
365
410
|
storeApiToken,
|
|
366
|
-
SCRYPT_COST,
|
|
367
411
|
hashPassword,
|
|
412
|
+
PasswordHasher,
|
|
413
|
+
ScryptHasher,
|
|
368
414
|
MIN_PASSWORD_LENGTH,
|
|
369
415
|
ADMIN_USER_ID,
|
|
370
416
|
generateInitialAdminPassword,
|
|
371
417
|
toUserInfo,
|
|
372
|
-
AuthService
|
|
418
|
+
AuthService,
|
|
419
|
+
InitialProjectProvisioner
|
|
373
420
|
};
|
|
374
|
-
//# sourceMappingURL=chunk-
|
|
421
|
+
//# sourceMappingURL=chunk-3I2XOPJB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth/password.ts","../src/auth/service.ts","../src/auth/api-token.ts","../src/http/errors.ts"],"sourcesContent":["import { Component, Interface } from \"@prismshadow/penguin-core/kernel\";\n/**\n * Password hashing (slow-hash storage).\n *\n * Uses node:crypto's scrypt (built-in, no extra dependency, meets the same\n * slow-hash requirement as bcrypt/argon2). Storage format:\n * `scrypt$N$r$p$<salt b64>$<hash b64>` — parameters are stored alongside the hash,\n * so old hashes remain verifiable after future parameter tuning; comparison uses\n * timingSafeEqual to guard against timing side-channels.\n */\nimport { randomBytes, scrypt, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Work factor for new hashes. `hashPassword` takes it as a defaulted argument so the server's\n * own test suite can hash at a token cost: almost every case there seeds an admin, provisions\n * users and logs them in, and one derivation at this strength costs on the order of 100ms.\n * Nothing on the production path passes the argument, and no configuration reaches it —\n * `buildAppDeps`, which carries the test override, is not part of this package's exports.\n */\nexport const SCRYPT_COST = 16384;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SALT_BYTES = 16;\nconst KEY_BYTES = 64;\n\nfunction scryptAsync(\n password: string,\n salt: Buffer,\n keyLen: number,\n n: number,\n r: number,\n p: number,\n): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n scrypt(password, salt, keyLen, { N: n, r, p, maxmem: 128 * 1024 * 1024 }, (err, key) => {\n if (err) reject(err);\n else resolve(key);\n });\n });\n}\n\n/**\n * Generates a password hash in `scrypt$N$r$p$salt$hash` format. `cost` is scrypt's N and\n * is recorded in the hash, so `verifyPassword` re-derives at whatever cost produced the\n * stored string.\n */\nexport async function hashPassword(password: string, cost: number = SCRYPT_COST): Promise<string> {\n const salt = randomBytes(SALT_BYTES);\n const key = await scryptAsync(password, salt, KEY_BYTES, cost, SCRYPT_R, SCRYPT_P);\n return [\n \"scrypt\",\n String(cost),\n String(SCRYPT_R),\n String(SCRYPT_P),\n salt.toString(\"base64\"),\n key.toString(\"base64\"),\n ].join(\"$\");\n}\n\n/** Verifies a password; returns false if the stored string has an invalid format (never throws, so the login path can uniformly treat it as a credential error). */\nexport async function verifyPassword(password: string, stored: string): Promise<boolean> {\n const parts = stored.split(\"$\");\n if (parts.length !== 6 || parts[0] !== \"scrypt\") return false;\n const n = Number(parts[1]);\n const r = Number(parts[2]);\n const p = Number(parts[3]);\n if (!Number.isInteger(n) || !Number.isInteger(r) || !Number.isInteger(p)) return false;\n let salt: Buffer;\n let expected: Buffer;\n try {\n salt = Buffer.from(parts[4]!, \"base64\");\n expected = Buffer.from(parts[5]!, \"base64\");\n } catch {\n return false;\n }\n if (salt.length === 0 || expected.length === 0) return false;\n let actual: Buffer;\n try {\n actual = await scryptAsync(password, salt, expected.length, n, r, p);\n } catch {\n return false;\n }\n return actual.length === expected.length && timingSafeEqual(actual, expected);\n}\n\n/** Hashes the passwords this server writes; a test stands in a cheap one. */\nexport abstract class PasswordHasher extends Interface<{\n hash(password: string): Promise<string>;\n}>() {}\n\n/** scrypt at full strength. */\n@Component()\nexport class ScryptHasher implements PasswordHasher {\n hash(password: string): Promise<string> {\n return hashPassword(password, SCRYPT_COST);\n }\n}\n","/**\n * Auth service: admin seeding / login / logout / password change / session validation.\n *\n * No open registration: an empty users table seeds `admin` with a random password hashed and\n * discarded unseen, claimed through the first-login link; every other user is created by an\n * admin. Sessions are rows in auth_sessions (cookie holds the token, the row its sha256), so\n * they outlive a restart and renew in place.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { tokensEqual } from \"./api-token.js\";\nimport type { UserInfo } from \"../api/types.js\";\nimport { HttpError } from \"../http/errors.js\";\nimport type { UserRow } from \"../db/repos/users.js\";\nimport { sessionTokenHash } from \"../db/repos/auth-sessions.js\";\nimport type { SessionViaValue, AuthSessionsRepo } from \"../db/repos/auth-sessions.js\";\nimport type { AuthRuntimeState } from \"./runtime-state.js\";\nimport { verifyPassword } from \"./password.js\";\nimport type { PasswordHasher } from \"./password.js\";\nimport { Component, Use, Interface } from \"@prismshadow/penguin-core/kernel\";\nimport type { AuthState, Clock, Config } from \"../hmr/capabilities.js\";\nimport type { Auth, AuthSessions, Users } from \"../mechanisms/identity.js\";\n\nexport const MIN_PASSWORD_LENGTH = 8;\n\n/** Built-in admin user_id. */\nexport const ADMIN_USER_ID = \"admin\";\n\n/**\n * Login throttling, per userId. After LOGIN_FREE_ATTEMPTS failures each attempt waits an\n * exponentially growing delay (1s doubling to a 60s cap), settling at a guess per minute.\n * Kept for nonexistent userIds too, so it is not an account-existence oracle. Deliberately NOT\n * covered: a concurrent burst before the first failure lands, and guesses spread across many\n * accounts.\n */\nconst LOGIN_FREE_ATTEMPTS = 5;\nconst LOGIN_BACKOFF_START_MS = 1000;\nconst LOGIN_BACKOFF_CAP_MS = 60_000;\n/** Failure entries idle longer than this are swept (bounds the map; far above the cap). */\nconst LOGIN_FAILURE_IDLE_MS = 15 * 60_000;\n\n/** Characters of base64url in a seeded password — 18 random bytes, 144 bits. */\nconst SEED_PASSWORD_CHARS = 24;\n\n/**\n * Never read or typed — hashed at once and discarded — which is what lets it be long enough\n * that the login endpoint, reachable the moment the account exists, has no search space.\n */\nexport function generateInitialAdminPassword(): string {\n return randomBytes(SEED_PASSWORD_CHARS).toString(\"base64url\").slice(0, SEED_PASSWORD_CHARS);\n}\n\n/**\n * \"desktop\" and \"setup\" may set a password without the old one (theirs is random and was never\n * shown); desktop-only routes open to \"desktop\" alone. \"token\" is the local API token's Bearer\n * header — per request, never a stored row. Anything else reads as \"password\".\n */\nexport type SessionVia = \"password\" | \"desktop\" | \"setup\" | \"token\";\n\n/** Row -> DTO. The two profile columns are omitted rather than sent as null when unset. */\nexport function toUserInfo(row: UserRow): UserInfo {\n return {\n userId: row.userId,\n isAdmin: row.isAdmin,\n passwordIsInitial: row.passwordIsInitial,\n ...(row.displayName !== null ? { displayName: row.displayName } : {}),\n ...(row.avatar !== null ? { avatar: row.avatar } : {}),\n createdAt: row.createdAt,\n };\n}\n\n@Component()\nexport class AuthService implements Auth {\n @Use() private readonly users!: Users;\n @Use() private readonly authSessions!: AuthSessions;\n /**\n * Process-scoped auth values (runtime-state.ts). Held by the RUNTIME so the link a boot\n * printed survives a platform push; everything else about auth is rebuilt with the App.\n */\n @Use() private readonly state!: AuthState;\n /** Provisions the initial Project at signup — declared at the consumer, below. */\n @Use() private readonly provisioner!: InitialProjectProvisioner;\n @Use() private readonly config!: Config;\n @Use() private readonly clock!: Clock;\n @Use() private readonly hasher!: PasswordHasher;\n /** Session lifetime, for the cookie that must expire WITH the session, not before it. */\n get sessionTtlMs(): number {\n return this.config.authSessionTtlMs;\n }\n private get sessionRenewMs(): number {\n return this.config.authSessionRenewMs;\n }\n /** Fixed initial password for the seeded admin; null generates a random one at seed time. */\n private get seedAdminPassword(): string | null {\n return this.config.seedAdminPassword;\n }\n\n setup(): void {\n this.authSessions.deleteExpired(this.clock.now().toISOString());\n }\n\n /**\n * Null once the server is claimed, so the only setup session that exists is a printed one.\n * On demand rather than in the constructor: seedAdmin() runs after it, so \"claimed?\" has no\n * answer there. A cached link whose row is gone is re-minted, not handed out dead.\n */\n mintFirstLogin(): string | null {\n if (!this.adminPasswordIsInitial()) return null;\n if (\n this.state.firstLoginToken !== null &&\n this.authenticateWithMeta(this.state.firstLoginToken) === null\n ) {\n this.state.firstLoginToken = null;\n }\n this.state.firstLoginToken ??= this.issueSession(ADMIN_USER_ID, \"setup\");\n return this.state.firstLoginToken;\n }\n\n /**\n * For the startup notice's pinned-seed gate (index.ts): a pin usually means the operator\n * knows the password, but an offline reset makes it stale, and the gate must see through\n * that or the rescue link stays suppressed.\n */\n async adminPasswordIs(password: string): Promise<boolean> {\n const row = this.users.findById(ADMIN_USER_ID);\n return row !== null && (await verifyPassword(password, row.passwordHash));\n }\n\n /**\n * Startup seeding (idempotent): creates the built-in admin and adopts default_project when\n * the users table is empty; if the initial Project fails, the user row is rolled back and\n * the server retries on next startup.\n */\n async seedAdmin(): Promise<void> {\n if (this.users.count() > 0) return;\n const password = this.seedAdminPassword ?? generateInitialAdminPassword();\n // A pinned override must meet the same policy as every other password — rejected before\n // any insert, so a configuration typo cannot create a trivially weak privileged account.\n if (password.length < MIN_PASSWORD_LENGTH) {\n throw new Error(\n `PENGUIN_SEED_ADMIN_PASSWORD must be at least ${MIN_PASSWORD_LENGTH} characters.`,\n );\n }\n const user: UserRow = {\n userId: ADMIN_USER_ID,\n passwordHash: await this.hasher.hash(password),\n isAdmin: true,\n passwordIsInitial: true,\n displayName: null,\n avatar: null,\n createdAt: this.clock.now().toISOString(),\n };\n this.users.insert(user);\n try {\n await this.provisioner.provisionInitialProject(user, true);\n } catch (err) {\n this.users.delete(user.userId);\n throw err;\n }\n }\n\n /**\n * Compared against the printed value, not merely verified: a cookie made out of ANY valid\n * token would let one person hand another a link into the sender's account. Not single-use,\n * because a prefetching browser would burn it before its reader clicked.\n */\n redeemFirstLogin(given: string): string | null {\n const expected = this.state.firstLoginToken;\n if (expected === null || given === \"\" || !tokensEqual(given, expected)) return null;\n return this.authenticateWithMeta(expected) === null ? null : expected;\n }\n\n /** Whether the built-in admin still runs on its initial password (gates the first-login link). */\n adminPasswordIsInitial(): boolean {\n return this.users.findById(ADMIN_USER_ID)?.passwordIsInitial === true;\n }\n\n /** Consecutive login failures per userId (see the throttling comment on the constants). */\n private readonly loginFailures = new Map<string, { failures: number; lastFailureAt: number }>();\n\n /** The wait imposed after `failures` consecutive failures (0 while within the free attempts). */\n private loginDelayMs(failures: number): number {\n const excess = failures - LOGIN_FREE_ATTEMPTS;\n if (excess <= 0) return 0;\n return Math.min(LOGIN_BACKOFF_START_MS * 2 ** (excess - 1), LOGIN_BACKOFF_CAP_MS);\n }\n\n async login(userId: string, password: string): Promise<{ user: UserInfo; token: string }> {\n const nowMs = this.clock.now().getTime();\n for (const [key, entry] of this.loginFailures) {\n if (nowMs - entry.lastFailureAt > LOGIN_FAILURE_IDLE_MS) this.loginFailures.delete(key);\n }\n const failed = this.loginFailures.get(userId);\n if (failed) {\n const readyAt = failed.lastFailureAt + this.loginDelayMs(failed.failures);\n if (nowMs < readyAt) {\n throw new HttpError(\n 429,\n \"too_many_attempts\",\n `Too many failed sign-in attempts. Try again in ${Math.ceil((readyAt - nowMs) / 1000)}s.`,\n );\n }\n }\n const row = this.users.findById(userId);\n const ok = row !== null && (await verifyPassword(password, row.passwordHash));\n if (!row || !ok) {\n this.loginFailures.set(userId, {\n failures: (failed?.failures ?? 0) + 1,\n lastFailureAt: this.clock.now().getTime(),\n });\n throw new HttpError(401, \"invalid_credentials\", \"Incorrect username or password.\");\n }\n this.loginFailures.delete(userId);\n this.authSessions.deleteExpired(this.clock.now().toISOString());\n return { user: toUserInfo(row), token: this.issueSession(row.userId, \"password\") };\n }\n\n /**\n * Desktop-mode sign-in: an admin session with no password check — the claim route already\n * redeemed the shell's one-shot token, which is the credential here. Throws only on a\n * broken deployment (seeding runs before the route exists).\n */\n loginDesktop(): { user: UserInfo; token: string } {\n const row = this.users.findById(ADMIN_USER_ID);\n if (!row) {\n throw new HttpError(500, \"internal\", \"Built-in admin has not been seeded.\");\n }\n return { user: toUserInfo(row), token: this.issueSession(row.userId, \"desktop\") };\n }\n\n /** Self password change (user settings): validates the old password first; the current session stays valid. */\n async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void> {\n const row = this.users.findById(userId);\n if (!row || !(await verifyPassword(oldPassword, row.passwordHash))) {\n throw new HttpError(400, \"password_mismatch\", \"Current password is incorrect.\");\n }\n await this.setPassword(userId, newPassword);\n }\n\n /** No old-password check; the me route gates which sessions may call this. */\n async setInitialPassword(userId: string, newPassword: string): Promise<void> {\n await this.setPassword(userId, newPassword);\n }\n\n /**\n * Validation comes FIRST, so a rejected password leaves the first-login link alive: burning\n * it on a typo would strand the claimer until a restart.\n */\n private async setPassword(userId: string, newPassword: string): Promise<void> {\n if (newPassword.length < MIN_PASSWORD_LENGTH) {\n throw new HttpError(400, \"invalid_password\", \"Password must be at least 8 characters.\");\n }\n this.users.updatePassword(userId, await this.hasher.hash(newPassword), false);\n // A successful set proves at least what the successful login that resets the counter\n // proves (the old password, or a desktop/setup session), so it resets it too. Without\n // this, anyone spamming the login endpoint holds the backoff window open, and the sign-in\n // that follows a first-login claim (routes/me.ts) would 429 AFTER the password committed —\n // reporting failure for a change that took, with the claimer's setup session already gone.\n this.loginFailures.delete(userId);\n // EVERY first-login session for this account, not just the link this process printed: an\n // earlier boot's link can still be live in a terminal scrollback, and a `setup` session is\n // allowed to change the password without knowing the old one — so one left behind is an\n // account takeover waiting to happen.\n if (userId === ADMIN_USER_ID) {\n this.authSessions.deleteByUserAndVia(userId, \"setup\");\n this.state.firstLoginToken = null;\n }\n }\n\n /** Ends a session: the row is the session, so logout deletes it. Unknown token is a no-op. */\n logout(token: string): void {\n this.authSessions.delete(sessionTokenHash(token));\n }\n\n /**\n * The current boot's local API token (what control-env injection hands to tool\n * subprocesses); null when none was minted. It lives on the runtime state, not on this\n * service: the token was written to `<root>/api-token` by the process, so it must\n * outlive the App a push replaces (auth/runtime-state.ts).\n */\n localApiToken(): string | null {\n return this.state.apiToken;\n }\n\n /**\n * Validates a Bearer token against the boot's local API token (constant-time compare):\n * a match authenticates as the built-in admin — holding the token proves filesystem\n * access to the data root, which is admin authority (see auth/api-token.ts). Returns\n * null on mismatch, when no token was minted, or when the admin row is missing.\n */\n authenticateApiToken(token: string): { user: UserRow; via: SessionVia } | null {\n const apiToken = this.state.apiToken;\n if (apiToken === null || token.length === 0) return null;\n if (!tokensEqual(token, apiToken)) return null;\n const user = this.users.findById(ADMIN_USER_ID);\n return user === null ? null : { user, via: \"token\" };\n }\n\n /**\n * Validates the cookie token. Sliding renewal tops the expiry up IN PLACE, so the cookie\n * value never changes and there is no second identity to revoke. Only a session whose own\n * span reaches the renewal window slides — a one-hour minted token must expire at its hour,\n * not stretch by being used.\n */\n authenticateWithMeta(token: string): { user: UserRow; via: SessionVia; renewed: boolean } | null {\n const tokenHash = sessionTokenHash(token);\n const session = this.authSessions.findByTokenHash(tokenHash);\n if (!session) return null;\n const now = this.clock.now();\n const expiresAt = Date.parse(session.expiresAt);\n if (expiresAt <= now.getTime()) {\n this.authSessions.delete(tokenHash);\n return null;\n }\n const user = this.users.findById(session.userId);\n if (!user) return null;\n const renewable = expiresAt - Date.parse(session.createdAt) >= this.sessionRenewMs;\n let renewed = false;\n if (renewable && expiresAt - now.getTime() < this.sessionRenewMs) {\n this.authSessions.touch(tokenHash, new Date(now.getTime() + this.sessionTtlMs).toISOString());\n renewed = true;\n }\n const via: SessionVia =\n session.via === \"desktop\" ? \"desktop\" : session.via === \"setup\" ? \"setup\" : \"password\";\n return { user, via, renewed };\n }\n\n private issueSession(userId: string, via: SessionViaValue): string {\n return this.authSessions.issue({\n userId,\n via,\n now: this.clock.now(),\n ttlMs: this.sessionTtlMs,\n }).token;\n }\n}\n\n/** What signing a new user in needs of project administration — declared at the consumer. */\nexport abstract class InitialProjectProvisioner extends Interface<{\n provisionInitialProject(user: UserRow, isAdmin: boolean): Promise<void>;\n}>() {}\n","/**\n * Local API token (`<root>/api-token`): the machine-local credential behind\n * `Authorization: Bearer`.\n *\n * Minted fresh at every server boot (same recipe as auth-session tokens) and written to\n * the data root with owner-only permissions, so anything that can read the file can call\n * the API as the admin. That equivalence is the authorization model, not an accident:\n * local filesystem access to the data root already IS admin authority — the same rule\n * `penguin server reset-admin-password` stands on (whoever can run it owns web.db\n * anyway) — and agents driving their own harness through the CLI is the product feature\n * this token exists for (server-driven Sessions hand it to tool subprocesses as\n * PENGUIN_API_TOKEN). Rotation per boot bounds the life of any leaked copy to the\n * process that minted it.\n */\nimport { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport function apiTokenPath(root: string): string {\n return path.join(root, \"api-token\");\n}\n\n/** Mints a boot-scoped local API token (the auth-session token recipe). */\nexport function mintApiToken(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/**\n * Persists the boot token (owner-only file, tmp + rename so a concurrent reader never\n * sees a partial write). Best-effort like the initial-password file: an exotic read-only\n * root must not stop the server — local CLI callers then fall back to\n * PENGUIN_API_TOKEN.\n */\nexport function storeApiToken(root: string, token: string): void {\n try {\n fs.mkdirSync(root, { recursive: true });\n const target = apiTokenPath(root);\n const tmp = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(tmp, `${token}\\n`, { mode: 0o600 });\n fs.renameSync(tmp, target);\n } catch {\n // Best-effort: Bearer auth still works for callers holding the token some other way.\n }\n}\n\n/** The stored token, or null when absent/unreadable/empty (shared with the CLI's file fallback). */\nexport function readApiToken(root: string): string | null {\n try {\n const value = fs.readFileSync(apiTokenPath(root), \"utf8\").trim();\n return value === \"\" ? null : value;\n } catch {\n return null;\n }\n}\n\n/**\n * Constant-time equality for token checks: both sides are reduced to fixed-size sha256\n * digests first, so the comparison's timing depends on neither the content nor the\n * length of an attacker-supplied value.\n */\nexport function tokensEqual(a: string, b: string): boolean {\n const digest = (v: string): Buffer => createHash(\"sha256\").update(v).digest();\n return timingSafeEqual(digest(a), digest(b));\n}\n","/**\n * Unified HTTP error: `{error: {code, message}}` response body,\n * with message in English.\n *\n * Routes and services express business errors via throw HttpError; app-level onError\n * uniformly converges these into a JSON response, with unknown errors converged to 500\n * (never leaking internal details to the client).\n */\nimport type { Context } from \"hono\";\nimport type { ErrorBody } from \"../api/types.js\";\n\nexport class HttpError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\nexport function errorBody(code: string, message: string): ErrorBody {\n return { error: { code, message } };\n}\n\n/**\n * Model missing a credential: the provider SDK throws this at **client-construction\n * time** (hit by both creating a Session and resuming a Session); the original message\n * is full of environment variable names, meaningless to a user — uniformly replaced\n * with a single actionable sentence. The frontend produces localized text by code\n * (message is only a fallback); see web's lib/api-error.ts.\n */\nexport function isMissingCredential(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n return /missing credentials|api[_ ]?key/i.test(message);\n}\n\nexport function modelCredentialMissing(modelId: string): HttpError {\n return new HttpError(\n 400,\n \"model_credential_missing\",\n `Model ${modelId} has no API key yet. Configure it on the Models page first.`,\n );\n}\n\n/** app.onError handler: maps HttpError through as-is; everything else is logged and converged to 500. */\nexport function handleError(err: Error, c: Context): Response {\n if (err instanceof HttpError) {\n return c.json(errorBody(err.code, err.message), err.status as 400);\n }\n // Unknown error: print the stack for diagnosis, but never expose details externally.\n console.error(`[server] Unhandled exception: ${err.stack ?? err.message}`);\n return c.json(errorBody(\"internal\", \"Internal server error.\"), 500);\n}\n"],"mappings":"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","names":["randomBytes","randomBytes","timingSafeEqual","timingSafeEqual","Component","Interface","randomBytes","_init","Component","Interface"]}
|