@chfischerx/puttry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +41 -0
- package/LICENSE +21 -0
- package/README.md +340 -0
- package/dist/assets/favicon-B8wORTBw.svg +13 -0
- package/dist/assets/index-B7k6QJRs.css +1 -0
- package/dist/assets/index-BDkarAkX.js +2 -0
- package/dist/assets/index-DrvzA-mO.js +195 -0
- package/dist/index.html +14 -0
- package/dist-server/cli.js +1750 -0
- package/dist-server/server.js +4879 -0
- package/package.json +74 -0
|
@@ -0,0 +1,1750 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/server/logger.ts
|
|
13
|
+
import winston from "winston";
|
|
14
|
+
function getLogger() {
|
|
15
|
+
if (logger) return logger;
|
|
16
|
+
const level = process.env.VERBOSE === "1" || process.env.VERBOSE === "true" ? "debug" : "info";
|
|
17
|
+
const fmt = printf(
|
|
18
|
+
({ level: level2, message, timestamp: timestamp2 }) => `${timestamp2} [${level2}] ${message}`
|
|
19
|
+
);
|
|
20
|
+
logger = winston.createLogger({
|
|
21
|
+
level,
|
|
22
|
+
format: combine(timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), fmt),
|
|
23
|
+
transports: [new winston.transports.Console()]
|
|
24
|
+
});
|
|
25
|
+
if (level === "debug") {
|
|
26
|
+
logger.debug("[logger] Debug level logging ENABLED");
|
|
27
|
+
}
|
|
28
|
+
return logger;
|
|
29
|
+
}
|
|
30
|
+
var combine, timestamp, printf, logger, logger_default;
|
|
31
|
+
var init_logger = __esm({
|
|
32
|
+
"src/server/logger.ts"() {
|
|
33
|
+
({ combine, timestamp, printf } = winston.format);
|
|
34
|
+
logger = null;
|
|
35
|
+
logger_default = new Proxy(
|
|
36
|
+
{},
|
|
37
|
+
{
|
|
38
|
+
get(_target, prop) {
|
|
39
|
+
return getLogger()[prop];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// src/server/settings-api.ts
|
|
47
|
+
var settings_api_exports = {};
|
|
48
|
+
__export(settings_api_exports, {
|
|
49
|
+
SETTINGS_REGISTRY: () => SETTINGS_REGISTRY,
|
|
50
|
+
config: () => config,
|
|
51
|
+
getEnvFilePath: () => getEnvFilePath,
|
|
52
|
+
initializeConfig: () => initializeConfig,
|
|
53
|
+
parseEnvFile: () => parseEnvFile,
|
|
54
|
+
updateSetting: () => updateSetting,
|
|
55
|
+
writeEnvFile: () => writeEnvFile
|
|
56
|
+
});
|
|
57
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
58
|
+
import { join } from "node:path";
|
|
59
|
+
import { homedir } from "node:os";
|
|
60
|
+
function getEnvFilePath() {
|
|
61
|
+
const localPath = join(import.meta.dirname, "../../.env.local");
|
|
62
|
+
if (existsSync(localPath)) {
|
|
63
|
+
return localPath;
|
|
64
|
+
}
|
|
65
|
+
return join(homedir(), ".puttry", ".env");
|
|
66
|
+
}
|
|
67
|
+
function parseEnvFile(filePath) {
|
|
68
|
+
if (!existsSync(filePath)) {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
const content = readFileSync(filePath, "utf-8");
|
|
72
|
+
const result = {};
|
|
73
|
+
for (const line of content.split("\n")) {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
76
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
77
|
+
if (key) {
|
|
78
|
+
result[key] = valueParts.join("=");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
function writeEnvFile(filePath, data) {
|
|
85
|
+
const lines = Object.entries(data).map(([key, value]) => `${key}=${value}`).join("\n");
|
|
86
|
+
writeFileSync(filePath, lines + "\n", "utf-8");
|
|
87
|
+
}
|
|
88
|
+
function initializeConfig() {
|
|
89
|
+
config.AUTH_DISABLED = process.env.AUTH_DISABLED === "1" || process.env.AUTH_DISABLED === "true";
|
|
90
|
+
config.SHOW_AUTH_DISABLED_WARNING = process.env.SHOW_AUTH_DISABLED_WARNING === "1" || process.env.SHOW_AUTH_DISABLED_WARNING === "true";
|
|
91
|
+
config.TOTP_ENABLED = process.env.TOTP_ENABLED === "1" || process.env.TOTP_ENABLED === "true";
|
|
92
|
+
config.SESSION_PASSWORD_TYPE = process.env.SESSION_PASSWORD_TYPE ?? "xkcd";
|
|
93
|
+
config.SESSION_PASSWORD_LENGTH = Number(process.env.SESSION_PASSWORD_LENGTH ?? 4);
|
|
94
|
+
config.LOG_SESSION_PASSWORD = process.env.LOG_SESSION_PASSWORD !== "0";
|
|
95
|
+
config.PASSKEY_RP_ORIGIN = process.env.PASSKEY_RP_ORIGIN ?? "";
|
|
96
|
+
config.RATE_LIMIT_GLOBAL_MAX = Number(process.env.RATE_LIMIT_GLOBAL_MAX ?? 500);
|
|
97
|
+
config.RATE_LIMIT_SESSION_PASSWORD_MAX = Number(process.env.RATE_LIMIT_SESSION_PASSWORD_MAX ?? 10);
|
|
98
|
+
config.SCROLLBACK_LINES = Number(process.env.SCROLLBACK_LINES ?? 1e4);
|
|
99
|
+
}
|
|
100
|
+
function updateSetting(key, value) {
|
|
101
|
+
if (!(key in config)) {
|
|
102
|
+
return { success: false, error: `Unknown setting: ${key}` };
|
|
103
|
+
}
|
|
104
|
+
const configKey = key;
|
|
105
|
+
const metadata = SETTINGS_REGISTRY[configKey];
|
|
106
|
+
if (!metadata) {
|
|
107
|
+
return { success: false, error: `Setting not registered: ${key}` };
|
|
108
|
+
}
|
|
109
|
+
let convertedValue = value;
|
|
110
|
+
if (metadata.type === "boolean") {
|
|
111
|
+
convertedValue = value === "true" || value === "1" ? true : false;
|
|
112
|
+
} else if (metadata.type === "number") {
|
|
113
|
+
convertedValue = Number(value);
|
|
114
|
+
if (isNaN(convertedValue)) {
|
|
115
|
+
return { success: false, error: `Invalid number for ${key}` };
|
|
116
|
+
}
|
|
117
|
+
} else if (metadata.type === "enum" && "values" in metadata) {
|
|
118
|
+
const validValues = metadata.values;
|
|
119
|
+
if (!validValues.includes(value)) {
|
|
120
|
+
return { success: false, error: `Invalid value for ${key}. Must be one of: ${validValues.join(", ")}` };
|
|
121
|
+
}
|
|
122
|
+
convertedValue = value;
|
|
123
|
+
}
|
|
124
|
+
;
|
|
125
|
+
config[configKey] = convertedValue;
|
|
126
|
+
if (metadata.type === "boolean") {
|
|
127
|
+
process.env[configKey] = convertedValue ? "1" : "0";
|
|
128
|
+
} else {
|
|
129
|
+
process.env[configKey] = String(convertedValue);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const envPath = getEnvFilePath();
|
|
133
|
+
const envData = parseEnvFile(envPath);
|
|
134
|
+
envData[configKey] = process.env[configKey];
|
|
135
|
+
writeEnvFile(envPath, envData);
|
|
136
|
+
logger_default.info(`[settings] Updated ${configKey} = ${process.env[configKey]}`);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
logger_default.error(`[settings] Failed to persist ${configKey}:`, err);
|
|
139
|
+
return { success: false, error: `Failed to persist setting: ${err}` };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
requiresRestart: false,
|
|
144
|
+
note: metadata.note
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
var config, SETTINGS_REGISTRY;
|
|
148
|
+
var init_settings_api = __esm({
|
|
149
|
+
"src/server/settings-api.ts"() {
|
|
150
|
+
init_logger();
|
|
151
|
+
config = {
|
|
152
|
+
AUTH_DISABLED: false,
|
|
153
|
+
SHOW_AUTH_DISABLED_WARNING: false,
|
|
154
|
+
TOTP_ENABLED: false,
|
|
155
|
+
SESSION_PASSWORD_TYPE: "xkcd",
|
|
156
|
+
SESSION_PASSWORD_LENGTH: 4,
|
|
157
|
+
LOG_SESSION_PASSWORD: true,
|
|
158
|
+
PASSKEY_RP_ORIGIN: "",
|
|
159
|
+
RATE_LIMIT_GLOBAL_MAX: 500,
|
|
160
|
+
RATE_LIMIT_SESSION_PASSWORD_MAX: 10,
|
|
161
|
+
SCROLLBACK_LINES: 1e4
|
|
162
|
+
};
|
|
163
|
+
SETTINGS_REGISTRY = {
|
|
164
|
+
AUTH_DISABLED: { type: "boolean", live: true, requiresRestart: false, note: "Users must reload" },
|
|
165
|
+
SHOW_AUTH_DISABLED_WARNING: { type: "boolean", live: true, requiresRestart: false },
|
|
166
|
+
TOTP_ENABLED: { type: "boolean", live: true, requiresRestart: false, note: "Affects next login attempt" },
|
|
167
|
+
SESSION_PASSWORD_TYPE: { type: "enum", values: ["xkcd", "random"], live: true, requiresRestart: false },
|
|
168
|
+
SESSION_PASSWORD_LENGTH: { type: "number", live: true, requiresRestart: false },
|
|
169
|
+
LOG_SESSION_PASSWORD: { type: "boolean", live: true, requiresRestart: false },
|
|
170
|
+
PASSKEY_RP_ORIGIN: { type: "string", live: true, requiresRestart: false },
|
|
171
|
+
RATE_LIMIT_GLOBAL_MAX: { type: "number", live: true, requiresRestart: false },
|
|
172
|
+
RATE_LIMIT_SESSION_PASSWORD_MAX: { type: "number", live: true, requiresRestart: false },
|
|
173
|
+
SCROLLBACK_LINES: { type: "number", live: true, requiresRestart: false, note: "Affects new sessions" }
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// src/lib/password-gen.ts
|
|
179
|
+
function generateXkcdPassword(wordCount = 4) {
|
|
180
|
+
const selected = [];
|
|
181
|
+
for (let i = 0; i < wordCount; i++) {
|
|
182
|
+
const idx = Math.floor(Math.random() * WORDS.length);
|
|
183
|
+
selected.push(WORDS[idx]);
|
|
184
|
+
}
|
|
185
|
+
const digit = Math.floor(Math.random() * 10);
|
|
186
|
+
return `${selected.join("-")}-${digit}`;
|
|
187
|
+
}
|
|
188
|
+
function generateRandomPassword(length = 16) {
|
|
189
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
190
|
+
const bytes = new Uint8Array(length);
|
|
191
|
+
crypto.getRandomValues(bytes);
|
|
192
|
+
let password = "";
|
|
193
|
+
for (let i = 0; i < length; i++) {
|
|
194
|
+
password += chars[bytes[i] % chars.length];
|
|
195
|
+
}
|
|
196
|
+
return password;
|
|
197
|
+
}
|
|
198
|
+
var WORDS;
|
|
199
|
+
var init_password_gen = __esm({
|
|
200
|
+
"src/lib/password-gen.ts"() {
|
|
201
|
+
WORDS = [
|
|
202
|
+
"apple",
|
|
203
|
+
"baked",
|
|
204
|
+
"beach",
|
|
205
|
+
"bench",
|
|
206
|
+
"bikes",
|
|
207
|
+
"birds",
|
|
208
|
+
"black",
|
|
209
|
+
"blade",
|
|
210
|
+
"blank",
|
|
211
|
+
"blend",
|
|
212
|
+
"block",
|
|
213
|
+
"blood",
|
|
214
|
+
"blown",
|
|
215
|
+
"board",
|
|
216
|
+
"boats",
|
|
217
|
+
"books",
|
|
218
|
+
"boots",
|
|
219
|
+
"bound",
|
|
220
|
+
"boxes",
|
|
221
|
+
"bread",
|
|
222
|
+
"break",
|
|
223
|
+
"brick",
|
|
224
|
+
"bride",
|
|
225
|
+
"brief",
|
|
226
|
+
"bring",
|
|
227
|
+
"brink",
|
|
228
|
+
"broke",
|
|
229
|
+
"brown",
|
|
230
|
+
"build",
|
|
231
|
+
"built",
|
|
232
|
+
"cable",
|
|
233
|
+
"calls",
|
|
234
|
+
"cards",
|
|
235
|
+
"cargo",
|
|
236
|
+
"carol",
|
|
237
|
+
"carry",
|
|
238
|
+
"cases",
|
|
239
|
+
"catch",
|
|
240
|
+
"cause",
|
|
241
|
+
"caves",
|
|
242
|
+
"chain",
|
|
243
|
+
"chair",
|
|
244
|
+
"charm",
|
|
245
|
+
"chart",
|
|
246
|
+
"chase",
|
|
247
|
+
"cheap",
|
|
248
|
+
"check",
|
|
249
|
+
"chess",
|
|
250
|
+
"chest",
|
|
251
|
+
"chief",
|
|
252
|
+
"child",
|
|
253
|
+
"china",
|
|
254
|
+
"chose",
|
|
255
|
+
"claim",
|
|
256
|
+
"class",
|
|
257
|
+
"clean",
|
|
258
|
+
"clear",
|
|
259
|
+
"click",
|
|
260
|
+
"cliff",
|
|
261
|
+
"climb",
|
|
262
|
+
"clock",
|
|
263
|
+
"close",
|
|
264
|
+
"cloud",
|
|
265
|
+
"coach",
|
|
266
|
+
"coast",
|
|
267
|
+
"codes",
|
|
268
|
+
"coins",
|
|
269
|
+
"comet",
|
|
270
|
+
"comic",
|
|
271
|
+
"coral",
|
|
272
|
+
"cords",
|
|
273
|
+
"cores",
|
|
274
|
+
"craft",
|
|
275
|
+
"crash",
|
|
276
|
+
"cream",
|
|
277
|
+
"creek",
|
|
278
|
+
"crime",
|
|
279
|
+
"crops",
|
|
280
|
+
"cross",
|
|
281
|
+
"crowd",
|
|
282
|
+
"crown",
|
|
283
|
+
"crude",
|
|
284
|
+
"curve",
|
|
285
|
+
"cycle",
|
|
286
|
+
"daily",
|
|
287
|
+
"dance",
|
|
288
|
+
"darts",
|
|
289
|
+
"dated",
|
|
290
|
+
"deals",
|
|
291
|
+
"death",
|
|
292
|
+
"decks",
|
|
293
|
+
"delay",
|
|
294
|
+
"delta",
|
|
295
|
+
"dense",
|
|
296
|
+
"depth",
|
|
297
|
+
"derby",
|
|
298
|
+
"dials",
|
|
299
|
+
"diary",
|
|
300
|
+
"diced",
|
|
301
|
+
"diner",
|
|
302
|
+
"dirty",
|
|
303
|
+
"disco",
|
|
304
|
+
"ditch",
|
|
305
|
+
"diver",
|
|
306
|
+
"dodge",
|
|
307
|
+
"donor",
|
|
308
|
+
"doors",
|
|
309
|
+
"doubt",
|
|
310
|
+
"dough",
|
|
311
|
+
"draft",
|
|
312
|
+
"drain",
|
|
313
|
+
"drake",
|
|
314
|
+
"drank",
|
|
315
|
+
"drawn",
|
|
316
|
+
"dread",
|
|
317
|
+
"dream",
|
|
318
|
+
"dress",
|
|
319
|
+
"dried",
|
|
320
|
+
"drift",
|
|
321
|
+
"drill",
|
|
322
|
+
"drink",
|
|
323
|
+
"drive",
|
|
324
|
+
"drown",
|
|
325
|
+
"drugs",
|
|
326
|
+
"drums",
|
|
327
|
+
"drunk",
|
|
328
|
+
"ducks",
|
|
329
|
+
"dunes",
|
|
330
|
+
"eager",
|
|
331
|
+
"early",
|
|
332
|
+
"earth",
|
|
333
|
+
"easel",
|
|
334
|
+
"eaten",
|
|
335
|
+
"eater",
|
|
336
|
+
"edges",
|
|
337
|
+
"eight",
|
|
338
|
+
"elite",
|
|
339
|
+
"empty",
|
|
340
|
+
"endow",
|
|
341
|
+
"enemy",
|
|
342
|
+
"enjoy",
|
|
343
|
+
"enter",
|
|
344
|
+
"entry",
|
|
345
|
+
"epoch",
|
|
346
|
+
"equal",
|
|
347
|
+
"error",
|
|
348
|
+
"essay",
|
|
349
|
+
"ethos",
|
|
350
|
+
"event",
|
|
351
|
+
"every",
|
|
352
|
+
"exact",
|
|
353
|
+
"excel",
|
|
354
|
+
"exist",
|
|
355
|
+
"extra",
|
|
356
|
+
"fable",
|
|
357
|
+
"faced",
|
|
358
|
+
"facts",
|
|
359
|
+
"faded",
|
|
360
|
+
"fails",
|
|
361
|
+
"fairy",
|
|
362
|
+
"faith",
|
|
363
|
+
"falls",
|
|
364
|
+
"false",
|
|
365
|
+
"famed",
|
|
366
|
+
"fancy",
|
|
367
|
+
"farms",
|
|
368
|
+
"fatal",
|
|
369
|
+
"fault",
|
|
370
|
+
"fauna",
|
|
371
|
+
"favor",
|
|
372
|
+
"feast",
|
|
373
|
+
"feats",
|
|
374
|
+
"feeds",
|
|
375
|
+
"feels",
|
|
376
|
+
"fence",
|
|
377
|
+
"ferry",
|
|
378
|
+
"fetch",
|
|
379
|
+
"fever",
|
|
380
|
+
"fewer",
|
|
381
|
+
"fiber",
|
|
382
|
+
"field",
|
|
383
|
+
"fiend",
|
|
384
|
+
"fiery",
|
|
385
|
+
"fifth",
|
|
386
|
+
"fifty",
|
|
387
|
+
"fight",
|
|
388
|
+
"filed",
|
|
389
|
+
"files",
|
|
390
|
+
"fills",
|
|
391
|
+
"films",
|
|
392
|
+
"final",
|
|
393
|
+
"finds",
|
|
394
|
+
"fined",
|
|
395
|
+
"finer",
|
|
396
|
+
"fires",
|
|
397
|
+
"first",
|
|
398
|
+
"fists",
|
|
399
|
+
"fixed",
|
|
400
|
+
"flags",
|
|
401
|
+
"flame",
|
|
402
|
+
"flank",
|
|
403
|
+
"flaps",
|
|
404
|
+
"flash",
|
|
405
|
+
"flask",
|
|
406
|
+
"flats",
|
|
407
|
+
"flaws",
|
|
408
|
+
"fleas",
|
|
409
|
+
"fleet",
|
|
410
|
+
"flesh",
|
|
411
|
+
"flies",
|
|
412
|
+
"flint",
|
|
413
|
+
"float",
|
|
414
|
+
"flock",
|
|
415
|
+
"flood",
|
|
416
|
+
"floor",
|
|
417
|
+
"flour",
|
|
418
|
+
"flows",
|
|
419
|
+
"fluid",
|
|
420
|
+
"flush",
|
|
421
|
+
"foams",
|
|
422
|
+
"focal",
|
|
423
|
+
"focus",
|
|
424
|
+
"foggy",
|
|
425
|
+
"folds",
|
|
426
|
+
"folks",
|
|
427
|
+
"fonts",
|
|
428
|
+
"foods",
|
|
429
|
+
"fools",
|
|
430
|
+
"force",
|
|
431
|
+
"forge",
|
|
432
|
+
"forms",
|
|
433
|
+
"forth",
|
|
434
|
+
"forty",
|
|
435
|
+
"forum",
|
|
436
|
+
"fouls",
|
|
437
|
+
"found",
|
|
438
|
+
"fount",
|
|
439
|
+
"frame",
|
|
440
|
+
"frank",
|
|
441
|
+
"fraud",
|
|
442
|
+
"freak",
|
|
443
|
+
"fresh",
|
|
444
|
+
"fried",
|
|
445
|
+
"fries",
|
|
446
|
+
"frock",
|
|
447
|
+
"front",
|
|
448
|
+
"frost",
|
|
449
|
+
"frown",
|
|
450
|
+
"froze",
|
|
451
|
+
"fruit",
|
|
452
|
+
"fuels",
|
|
453
|
+
"fully",
|
|
454
|
+
"funds",
|
|
455
|
+
"fungi",
|
|
456
|
+
"funky",
|
|
457
|
+
"funny",
|
|
458
|
+
"fuzzy",
|
|
459
|
+
"gains",
|
|
460
|
+
"games",
|
|
461
|
+
"gangs",
|
|
462
|
+
"gates",
|
|
463
|
+
"gauge",
|
|
464
|
+
"gazed",
|
|
465
|
+
"gears",
|
|
466
|
+
"genus",
|
|
467
|
+
"ghost",
|
|
468
|
+
"giant",
|
|
469
|
+
"gifts",
|
|
470
|
+
"girls",
|
|
471
|
+
"given",
|
|
472
|
+
"gives",
|
|
473
|
+
"gland",
|
|
474
|
+
"glare",
|
|
475
|
+
"glass",
|
|
476
|
+
"glaze",
|
|
477
|
+
"gleam",
|
|
478
|
+
"glean",
|
|
479
|
+
"glide",
|
|
480
|
+
"glint",
|
|
481
|
+
"globe",
|
|
482
|
+
"gloom",
|
|
483
|
+
"glory",
|
|
484
|
+
"gloss",
|
|
485
|
+
"glove",
|
|
486
|
+
"glows",
|
|
487
|
+
"glued",
|
|
488
|
+
"gnome",
|
|
489
|
+
"goals",
|
|
490
|
+
"goats",
|
|
491
|
+
"going",
|
|
492
|
+
"golds",
|
|
493
|
+
"golfs",
|
|
494
|
+
"goose",
|
|
495
|
+
"gorge",
|
|
496
|
+
"gowns",
|
|
497
|
+
"grace",
|
|
498
|
+
"grade",
|
|
499
|
+
"graft",
|
|
500
|
+
"grain",
|
|
501
|
+
"grand",
|
|
502
|
+
"grant",
|
|
503
|
+
"grape",
|
|
504
|
+
"graph",
|
|
505
|
+
"grasp",
|
|
506
|
+
"grass",
|
|
507
|
+
"grate",
|
|
508
|
+
"grave",
|
|
509
|
+
"gravy",
|
|
510
|
+
"graze",
|
|
511
|
+
"great",
|
|
512
|
+
"greed",
|
|
513
|
+
"greek",
|
|
514
|
+
"green",
|
|
515
|
+
"greet",
|
|
516
|
+
"grief",
|
|
517
|
+
"grill",
|
|
518
|
+
"grime",
|
|
519
|
+
"grind",
|
|
520
|
+
"grins",
|
|
521
|
+
"gripe",
|
|
522
|
+
"grips",
|
|
523
|
+
"grist",
|
|
524
|
+
"grits",
|
|
525
|
+
"groan",
|
|
526
|
+
"groom",
|
|
527
|
+
"grope",
|
|
528
|
+
"gross",
|
|
529
|
+
"group",
|
|
530
|
+
"grove",
|
|
531
|
+
"growl",
|
|
532
|
+
"grown",
|
|
533
|
+
"grows",
|
|
534
|
+
"guard",
|
|
535
|
+
"guess",
|
|
536
|
+
"guest",
|
|
537
|
+
"guide",
|
|
538
|
+
"guild",
|
|
539
|
+
"guilt",
|
|
540
|
+
"guise",
|
|
541
|
+
"gulfs",
|
|
542
|
+
"gulps",
|
|
543
|
+
"gummy",
|
|
544
|
+
"gusts",
|
|
545
|
+
"gutsy",
|
|
546
|
+
"habit",
|
|
547
|
+
"hails",
|
|
548
|
+
"hairs",
|
|
549
|
+
"halts",
|
|
550
|
+
"halve",
|
|
551
|
+
"hands",
|
|
552
|
+
"handy",
|
|
553
|
+
"hangs",
|
|
554
|
+
"happy",
|
|
555
|
+
"hardy",
|
|
556
|
+
"harem",
|
|
557
|
+
"harks",
|
|
558
|
+
"harms",
|
|
559
|
+
"harps",
|
|
560
|
+
"harsh",
|
|
561
|
+
"haste",
|
|
562
|
+
"hasty",
|
|
563
|
+
"hatch",
|
|
564
|
+
"hated",
|
|
565
|
+
"hater",
|
|
566
|
+
"hauls",
|
|
567
|
+
"haunt",
|
|
568
|
+
"haven",
|
|
569
|
+
"hawks",
|
|
570
|
+
"heads",
|
|
571
|
+
"heals",
|
|
572
|
+
"heaps",
|
|
573
|
+
"heard",
|
|
574
|
+
"hears",
|
|
575
|
+
"heart",
|
|
576
|
+
"heats",
|
|
577
|
+
"heavy",
|
|
578
|
+
"hedge",
|
|
579
|
+
"heeds",
|
|
580
|
+
"heels",
|
|
581
|
+
"heirs",
|
|
582
|
+
"heist",
|
|
583
|
+
"hello",
|
|
584
|
+
"helps",
|
|
585
|
+
"hence",
|
|
586
|
+
"herbs",
|
|
587
|
+
"herds",
|
|
588
|
+
"heron",
|
|
589
|
+
"hides",
|
|
590
|
+
"highs",
|
|
591
|
+
"hiked",
|
|
592
|
+
"hiker",
|
|
593
|
+
"hikes",
|
|
594
|
+
"hills",
|
|
595
|
+
"hinds",
|
|
596
|
+
"hinge",
|
|
597
|
+
"hints",
|
|
598
|
+
"hippo",
|
|
599
|
+
"hired",
|
|
600
|
+
"hires",
|
|
601
|
+
"hitch",
|
|
602
|
+
"hoard",
|
|
603
|
+
"hoary",
|
|
604
|
+
"hobby",
|
|
605
|
+
"hoist",
|
|
606
|
+
"holds",
|
|
607
|
+
"holes",
|
|
608
|
+
"holly",
|
|
609
|
+
"homes",
|
|
610
|
+
"honed",
|
|
611
|
+
"hones",
|
|
612
|
+
"honey",
|
|
613
|
+
"honks",
|
|
614
|
+
"hoods",
|
|
615
|
+
"hoofs",
|
|
616
|
+
"hooks",
|
|
617
|
+
"hoops",
|
|
618
|
+
"hoots",
|
|
619
|
+
"hoped",
|
|
620
|
+
"hopes",
|
|
621
|
+
"horns",
|
|
622
|
+
"horse",
|
|
623
|
+
"hosed",
|
|
624
|
+
"hoses",
|
|
625
|
+
"hosts",
|
|
626
|
+
"hotly",
|
|
627
|
+
"hound",
|
|
628
|
+
"hours",
|
|
629
|
+
"house",
|
|
630
|
+
"hovel",
|
|
631
|
+
"hover",
|
|
632
|
+
"howdy",
|
|
633
|
+
"howls",
|
|
634
|
+
"hubby",
|
|
635
|
+
"huffs",
|
|
636
|
+
"hulks",
|
|
637
|
+
"hulls",
|
|
638
|
+
"human",
|
|
639
|
+
"humid",
|
|
640
|
+
"humor",
|
|
641
|
+
"humps",
|
|
642
|
+
"hunks",
|
|
643
|
+
"hunts",
|
|
644
|
+
"hurls",
|
|
645
|
+
"hurry",
|
|
646
|
+
"hurts",
|
|
647
|
+
"husks",
|
|
648
|
+
"husky",
|
|
649
|
+
"hutch",
|
|
650
|
+
"hydro",
|
|
651
|
+
"hyena",
|
|
652
|
+
"hyper",
|
|
653
|
+
"icing",
|
|
654
|
+
"icons",
|
|
655
|
+
"ideal",
|
|
656
|
+
"ideas",
|
|
657
|
+
"idiom",
|
|
658
|
+
"idiot",
|
|
659
|
+
"idles",
|
|
660
|
+
"idols",
|
|
661
|
+
"igloo",
|
|
662
|
+
"image",
|
|
663
|
+
"imbue",
|
|
664
|
+
"imply",
|
|
665
|
+
"inbox",
|
|
666
|
+
"incur",
|
|
667
|
+
"index",
|
|
668
|
+
"inept",
|
|
669
|
+
"inert",
|
|
670
|
+
"infer",
|
|
671
|
+
"infos",
|
|
672
|
+
"ingot",
|
|
673
|
+
"inlet",
|
|
674
|
+
"inner",
|
|
675
|
+
"input",
|
|
676
|
+
"inset",
|
|
677
|
+
"inter",
|
|
678
|
+
"intro",
|
|
679
|
+
"irons",
|
|
680
|
+
"irony",
|
|
681
|
+
"issue",
|
|
682
|
+
"items",
|
|
683
|
+
"itchy",
|
|
684
|
+
"ivory",
|
|
685
|
+
"jacks",
|
|
686
|
+
"jails",
|
|
687
|
+
"james",
|
|
688
|
+
"jamps",
|
|
689
|
+
"japan",
|
|
690
|
+
"jared",
|
|
691
|
+
"jarred",
|
|
692
|
+
"jawed",
|
|
693
|
+
"jazzy",
|
|
694
|
+
"jeans",
|
|
695
|
+
"jeeps",
|
|
696
|
+
"jeers",
|
|
697
|
+
"jello",
|
|
698
|
+
"jelly",
|
|
699
|
+
"jenny",
|
|
700
|
+
"jerks",
|
|
701
|
+
"jerry",
|
|
702
|
+
"jesse",
|
|
703
|
+
"jests",
|
|
704
|
+
"jetty",
|
|
705
|
+
"jewel",
|
|
706
|
+
"jiffy",
|
|
707
|
+
"jihad",
|
|
708
|
+
"jimmy",
|
|
709
|
+
"jingo",
|
|
710
|
+
"jinks",
|
|
711
|
+
"jived",
|
|
712
|
+
"jiver",
|
|
713
|
+
"jives",
|
|
714
|
+
"jocks",
|
|
715
|
+
"joeys",
|
|
716
|
+
"joins",
|
|
717
|
+
"joint",
|
|
718
|
+
"joist",
|
|
719
|
+
"joked",
|
|
720
|
+
"joker",
|
|
721
|
+
"jokes",
|
|
722
|
+
"jolly",
|
|
723
|
+
"jonah",
|
|
724
|
+
"jones",
|
|
725
|
+
"joust",
|
|
726
|
+
"jowls",
|
|
727
|
+
"joyed",
|
|
728
|
+
"judge",
|
|
729
|
+
"juice",
|
|
730
|
+
"juicy",
|
|
731
|
+
"jumbo",
|
|
732
|
+
"jumps",
|
|
733
|
+
"jumpy",
|
|
734
|
+
"junco",
|
|
735
|
+
"junks",
|
|
736
|
+
"junky",
|
|
737
|
+
"juror",
|
|
738
|
+
"kails",
|
|
739
|
+
"karma",
|
|
740
|
+
"kayak",
|
|
741
|
+
"keels",
|
|
742
|
+
"keend",
|
|
743
|
+
"keens",
|
|
744
|
+
"keeps",
|
|
745
|
+
"kefir",
|
|
746
|
+
"keira",
|
|
747
|
+
"keith",
|
|
748
|
+
"kelly",
|
|
749
|
+
"kelps",
|
|
750
|
+
"kendo",
|
|
751
|
+
"kenny",
|
|
752
|
+
"kenya",
|
|
753
|
+
"kerns",
|
|
754
|
+
"kevin",
|
|
755
|
+
"khaki",
|
|
756
|
+
"kicks",
|
|
757
|
+
"kills",
|
|
758
|
+
"kilns",
|
|
759
|
+
"kilos",
|
|
760
|
+
"kilts",
|
|
761
|
+
"kinds",
|
|
762
|
+
"kinks",
|
|
763
|
+
"kirks",
|
|
764
|
+
"kites",
|
|
765
|
+
"kiths",
|
|
766
|
+
"kitty",
|
|
767
|
+
"knack",
|
|
768
|
+
"knave",
|
|
769
|
+
"knead",
|
|
770
|
+
"kneel",
|
|
771
|
+
"knelt",
|
|
772
|
+
"knife",
|
|
773
|
+
"knits",
|
|
774
|
+
"knobs",
|
|
775
|
+
"knock",
|
|
776
|
+
"knoll",
|
|
777
|
+
"knots",
|
|
778
|
+
"known",
|
|
779
|
+
"knows",
|
|
780
|
+
"koala",
|
|
781
|
+
"kraft",
|
|
782
|
+
"krill",
|
|
783
|
+
"label",
|
|
784
|
+
"labor",
|
|
785
|
+
"laced",
|
|
786
|
+
"laces",
|
|
787
|
+
"lacks",
|
|
788
|
+
"lager",
|
|
789
|
+
"lakes",
|
|
790
|
+
"lambs",
|
|
791
|
+
"lamed",
|
|
792
|
+
"lames",
|
|
793
|
+
"lamps",
|
|
794
|
+
"lance",
|
|
795
|
+
"lands",
|
|
796
|
+
"lanes",
|
|
797
|
+
"lanky",
|
|
798
|
+
"lapse",
|
|
799
|
+
"larch",
|
|
800
|
+
"lards",
|
|
801
|
+
"large",
|
|
802
|
+
"larks",
|
|
803
|
+
"laser",
|
|
804
|
+
"lasso",
|
|
805
|
+
"latch",
|
|
806
|
+
"laths",
|
|
807
|
+
"lathe",
|
|
808
|
+
"latte",
|
|
809
|
+
"lauds",
|
|
810
|
+
"laugh",
|
|
811
|
+
"laura",
|
|
812
|
+
"laved",
|
|
813
|
+
"laves",
|
|
814
|
+
"lawns",
|
|
815
|
+
"layer",
|
|
816
|
+
"layed",
|
|
817
|
+
"layne",
|
|
818
|
+
"leads",
|
|
819
|
+
"leafs",
|
|
820
|
+
"leafy",
|
|
821
|
+
"leaks",
|
|
822
|
+
"leaky",
|
|
823
|
+
"leans",
|
|
824
|
+
"leant",
|
|
825
|
+
"leaps",
|
|
826
|
+
"learn",
|
|
827
|
+
"lease",
|
|
828
|
+
"leash",
|
|
829
|
+
"least",
|
|
830
|
+
"leapt",
|
|
831
|
+
"leave",
|
|
832
|
+
"ledge",
|
|
833
|
+
"leech",
|
|
834
|
+
"leeds",
|
|
835
|
+
"leeks",
|
|
836
|
+
"leers",
|
|
837
|
+
"leery",
|
|
838
|
+
"lefts",
|
|
839
|
+
"lefty",
|
|
840
|
+
"legal",
|
|
841
|
+
"legit",
|
|
842
|
+
"legos",
|
|
843
|
+
"lemon",
|
|
844
|
+
"lemur",
|
|
845
|
+
"lends",
|
|
846
|
+
"lenis",
|
|
847
|
+
"lenos",
|
|
848
|
+
"leper",
|
|
849
|
+
"lepus",
|
|
850
|
+
"level",
|
|
851
|
+
"lever",
|
|
852
|
+
"lewis",
|
|
853
|
+
"libra",
|
|
854
|
+
"licks",
|
|
855
|
+
"lidos",
|
|
856
|
+
"liege",
|
|
857
|
+
"liens",
|
|
858
|
+
"lifes",
|
|
859
|
+
"lifts",
|
|
860
|
+
"light",
|
|
861
|
+
"likes",
|
|
862
|
+
"lilac",
|
|
863
|
+
"limbs",
|
|
864
|
+
"limey",
|
|
865
|
+
"limit",
|
|
866
|
+
"limns",
|
|
867
|
+
"limos",
|
|
868
|
+
"linas",
|
|
869
|
+
"lined",
|
|
870
|
+
"linen",
|
|
871
|
+
"liner",
|
|
872
|
+
"lines",
|
|
873
|
+
"lingo",
|
|
874
|
+
"lings",
|
|
875
|
+
"links",
|
|
876
|
+
"linos",
|
|
877
|
+
"lints",
|
|
878
|
+
"linty",
|
|
879
|
+
"lions",
|
|
880
|
+
"lipid",
|
|
881
|
+
"lisps",
|
|
882
|
+
"lists",
|
|
883
|
+
"litas",
|
|
884
|
+
"lithe",
|
|
885
|
+
"lived",
|
|
886
|
+
"liven",
|
|
887
|
+
"liver",
|
|
888
|
+
"lives",
|
|
889
|
+
"livre",
|
|
890
|
+
"loads",
|
|
891
|
+
"loafs",
|
|
892
|
+
"loams",
|
|
893
|
+
"loamy",
|
|
894
|
+
"loans",
|
|
895
|
+
"loath",
|
|
896
|
+
"lobby",
|
|
897
|
+
"lobed",
|
|
898
|
+
"lobes",
|
|
899
|
+
"lobos",
|
|
900
|
+
"local",
|
|
901
|
+
"lochs",
|
|
902
|
+
"locks",
|
|
903
|
+
"locus",
|
|
904
|
+
"loden",
|
|
905
|
+
"lodes",
|
|
906
|
+
"lodge",
|
|
907
|
+
"lofts",
|
|
908
|
+
"lofty",
|
|
909
|
+
"logan",
|
|
910
|
+
"logas",
|
|
911
|
+
"logic",
|
|
912
|
+
"logos",
|
|
913
|
+
"logue",
|
|
914
|
+
"loins",
|
|
915
|
+
"loire",
|
|
916
|
+
"loked",
|
|
917
|
+
"loken",
|
|
918
|
+
"loken",
|
|
919
|
+
"lokey",
|
|
920
|
+
"lolas",
|
|
921
|
+
"lolly",
|
|
922
|
+
"loman",
|
|
923
|
+
"lonas",
|
|
924
|
+
"loned",
|
|
925
|
+
"loner",
|
|
926
|
+
"lones",
|
|
927
|
+
"longa",
|
|
928
|
+
"longe",
|
|
929
|
+
"longs",
|
|
930
|
+
"loona",
|
|
931
|
+
"loons",
|
|
932
|
+
"loony",
|
|
933
|
+
"loops",
|
|
934
|
+
"loopy",
|
|
935
|
+
"loose",
|
|
936
|
+
"loosh",
|
|
937
|
+
"loots",
|
|
938
|
+
"looty",
|
|
939
|
+
"loped",
|
|
940
|
+
"loper",
|
|
941
|
+
"lopes",
|
|
942
|
+
"lopey",
|
|
943
|
+
"lopht",
|
|
944
|
+
"lopia",
|
|
945
|
+
"lopin",
|
|
946
|
+
"lopko",
|
|
947
|
+
"lopod",
|
|
948
|
+
"loppa",
|
|
949
|
+
"loppy",
|
|
950
|
+
"lopsy",
|
|
951
|
+
"loral",
|
|
952
|
+
"loran",
|
|
953
|
+
"loras",
|
|
954
|
+
"lorch",
|
|
955
|
+
"lords",
|
|
956
|
+
"lordy",
|
|
957
|
+
"lorem",
|
|
958
|
+
"lorer",
|
|
959
|
+
"lores",
|
|
960
|
+
"loret",
|
|
961
|
+
"lorey",
|
|
962
|
+
"loria",
|
|
963
|
+
"lorik",
|
|
964
|
+
"loris",
|
|
965
|
+
"lorna",
|
|
966
|
+
"lorne",
|
|
967
|
+
"lorns",
|
|
968
|
+
"lorny",
|
|
969
|
+
"loro",
|
|
970
|
+
"loron",
|
|
971
|
+
"loros",
|
|
972
|
+
"lorox",
|
|
973
|
+
"lorry",
|
|
974
|
+
"lorsa",
|
|
975
|
+
"lorsi",
|
|
976
|
+
"lorsy",
|
|
977
|
+
"lorta",
|
|
978
|
+
"lorto",
|
|
979
|
+
"lorty",
|
|
980
|
+
"lorus",
|
|
981
|
+
"lorva",
|
|
982
|
+
"lorve",
|
|
983
|
+
"lorvy",
|
|
984
|
+
"lorza",
|
|
985
|
+
"losah",
|
|
986
|
+
"losak",
|
|
987
|
+
"losar",
|
|
988
|
+
"losba",
|
|
989
|
+
"losca",
|
|
990
|
+
"losda",
|
|
991
|
+
"losde",
|
|
992
|
+
"losdy",
|
|
993
|
+
"losea",
|
|
994
|
+
"loseb",
|
|
995
|
+
"losec",
|
|
996
|
+
"losed",
|
|
997
|
+
"losee",
|
|
998
|
+
"losel",
|
|
999
|
+
"losem",
|
|
1000
|
+
"losen",
|
|
1001
|
+
"loser",
|
|
1002
|
+
"loses",
|
|
1003
|
+
"loset",
|
|
1004
|
+
"losev",
|
|
1005
|
+
"losew",
|
|
1006
|
+
"losex",
|
|
1007
|
+
"losey",
|
|
1008
|
+
"losez",
|
|
1009
|
+
"losfa",
|
|
1010
|
+
"losfd",
|
|
1011
|
+
"losfe",
|
|
1012
|
+
"losfh",
|
|
1013
|
+
"losfi",
|
|
1014
|
+
"losfk",
|
|
1015
|
+
"losfl",
|
|
1016
|
+
"losfm",
|
|
1017
|
+
"losfn",
|
|
1018
|
+
"losfo",
|
|
1019
|
+
"losfp",
|
|
1020
|
+
"losfq",
|
|
1021
|
+
"losfr",
|
|
1022
|
+
"losfs",
|
|
1023
|
+
"losft",
|
|
1024
|
+
"losfu",
|
|
1025
|
+
"losfv",
|
|
1026
|
+
"losfw",
|
|
1027
|
+
"losfx",
|
|
1028
|
+
"losfy",
|
|
1029
|
+
"losfz"
|
|
1030
|
+
];
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
// src/server/auth-state.ts
|
|
1035
|
+
var auth_state_exports = {};
|
|
1036
|
+
__export(auth_state_exports, {
|
|
1037
|
+
cleanupAuthState: () => cleanupAuthState,
|
|
1038
|
+
clear2FAState: () => clear2FAState,
|
|
1039
|
+
get2FAState: () => get2FAState,
|
|
1040
|
+
getSessionPassword: () => getSessionPassword,
|
|
1041
|
+
initAuthState: () => initAuthState,
|
|
1042
|
+
on2FAStateChange: () => on2FAStateChange,
|
|
1043
|
+
onPasswordRotated: () => onPasswordRotated,
|
|
1044
|
+
rotateSessionPassword: () => rotateSessionPassword,
|
|
1045
|
+
save2FAState: () => save2FAState
|
|
1046
|
+
});
|
|
1047
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2, watchFile, unlinkSync } from "node:fs";
|
|
1048
|
+
import { join as join2 } from "node:path";
|
|
1049
|
+
import { homedir as homedir2 } from "node:os";
|
|
1050
|
+
import { EventEmitter } from "node:events";
|
|
1051
|
+
async function initAuthState() {
|
|
1052
|
+
await authState.init();
|
|
1053
|
+
}
|
|
1054
|
+
function getSessionPassword() {
|
|
1055
|
+
return authState.getSessionPassword();
|
|
1056
|
+
}
|
|
1057
|
+
function rotateSessionPassword() {
|
|
1058
|
+
return authState.rotateSessionPassword();
|
|
1059
|
+
}
|
|
1060
|
+
function get2FAState() {
|
|
1061
|
+
return authState.get2FAState();
|
|
1062
|
+
}
|
|
1063
|
+
function save2FAState(state) {
|
|
1064
|
+
authState.save2FAState(state);
|
|
1065
|
+
}
|
|
1066
|
+
function clear2FAState() {
|
|
1067
|
+
authState.clear2FAState();
|
|
1068
|
+
}
|
|
1069
|
+
function onPasswordRotated(callback) {
|
|
1070
|
+
authState.onPasswordRotated(callback);
|
|
1071
|
+
}
|
|
1072
|
+
function on2FAStateChange(callback) {
|
|
1073
|
+
authState.on2FAStateChange(callback);
|
|
1074
|
+
}
|
|
1075
|
+
function cleanupAuthState() {
|
|
1076
|
+
authState.cleanup();
|
|
1077
|
+
}
|
|
1078
|
+
var STATE_DIR, SESSION_PASSWORD_PATH, TOTP_STATE_PATH, AuthState, authState;
|
|
1079
|
+
var init_auth_state = __esm({
|
|
1080
|
+
"src/server/auth-state.ts"() {
|
|
1081
|
+
init_password_gen();
|
|
1082
|
+
init_logger();
|
|
1083
|
+
STATE_DIR = join2(homedir2(), ".puttry");
|
|
1084
|
+
SESSION_PASSWORD_PATH = join2(STATE_DIR, "session-password.txt");
|
|
1085
|
+
TOTP_STATE_PATH = join2(STATE_DIR, "2fa-state.json");
|
|
1086
|
+
AuthState = class extends EventEmitter {
|
|
1087
|
+
sessionPassword = null;
|
|
1088
|
+
totpState = null;
|
|
1089
|
+
watchers = [];
|
|
1090
|
+
async init() {
|
|
1091
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
1092
|
+
this.loadSessionPassword();
|
|
1093
|
+
this.loadTotpState();
|
|
1094
|
+
this.watchFiles();
|
|
1095
|
+
}
|
|
1096
|
+
loadSessionPassword() {
|
|
1097
|
+
if (existsSync2(SESSION_PASSWORD_PATH)) {
|
|
1098
|
+
try {
|
|
1099
|
+
this.sessionPassword = readFileSync2(SESSION_PASSWORD_PATH, "utf-8").trim();
|
|
1100
|
+
logger_default.info(`[auth-state] Session password loaded from disk`);
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
logger_default.error(`[auth-state] Failed to load session password: ${err instanceof Error ? err.message : err}`);
|
|
1103
|
+
this.generateNewSessionPassword();
|
|
1104
|
+
}
|
|
1105
|
+
} else {
|
|
1106
|
+
this.generateNewSessionPassword();
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
generateNewSessionPassword() {
|
|
1110
|
+
const type = (process.env.SESSION_PASSWORD_TYPE || "xkcd").toLowerCase();
|
|
1111
|
+
const length = parseInt(process.env.SESSION_PASSWORD_LENGTH || "4", 10);
|
|
1112
|
+
if (type === "random") {
|
|
1113
|
+
this.sessionPassword = generateRandomPassword(length);
|
|
1114
|
+
} else {
|
|
1115
|
+
this.sessionPassword = generateXkcdPassword(length);
|
|
1116
|
+
}
|
|
1117
|
+
this.persistSessionPassword();
|
|
1118
|
+
logger_default.info(`[auth-state] Generated new session password`);
|
|
1119
|
+
}
|
|
1120
|
+
persistSessionPassword() {
|
|
1121
|
+
if (!this.sessionPassword) return;
|
|
1122
|
+
try {
|
|
1123
|
+
writeFileSync2(SESSION_PASSWORD_PATH, this.sessionPassword, { mode: 384 });
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
logger_default.error(`[auth-state] Failed to save session password: ${err instanceof Error ? err.message : err}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
loadTotpState() {
|
|
1129
|
+
if (existsSync2(TOTP_STATE_PATH)) {
|
|
1130
|
+
try {
|
|
1131
|
+
const data = readFileSync2(TOTP_STATE_PATH, "utf-8");
|
|
1132
|
+
this.totpState = JSON.parse(data);
|
|
1133
|
+
logger_default.info(`[auth-state] TOTP state loaded from ${TOTP_STATE_PATH}: verified=${this.totpState?.verified}`);
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
logger_default.error(`[auth-state] Failed to load TOTP state from ${TOTP_STATE_PATH}: ${err instanceof Error ? err.message : err}`);
|
|
1136
|
+
this.totpState = null;
|
|
1137
|
+
}
|
|
1138
|
+
} else {
|
|
1139
|
+
logger_default.info(`[auth-state] TOTP state file not found at ${TOTP_STATE_PATH}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
watchFiles() {
|
|
1143
|
+
watchFile(SESSION_PASSWORD_PATH, () => {
|
|
1144
|
+
const oldPassword = this.sessionPassword;
|
|
1145
|
+
this.loadSessionPassword();
|
|
1146
|
+
if (oldPassword !== this.sessionPassword) {
|
|
1147
|
+
logger_default.info(`[auth-state] Session password changed externally`);
|
|
1148
|
+
this.emit("passwordRotated");
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
watchFile(TOTP_STATE_PATH, () => {
|
|
1152
|
+
const oldState = this.totpState;
|
|
1153
|
+
this.loadTotpState();
|
|
1154
|
+
if (JSON.stringify(oldState) !== JSON.stringify(this.totpState)) {
|
|
1155
|
+
logger_default.info(`[auth-state] TOTP state changed externally`);
|
|
1156
|
+
this.emit("2faChanged");
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
getSessionPassword() {
|
|
1161
|
+
if (!this.sessionPassword) {
|
|
1162
|
+
this.generateNewSessionPassword();
|
|
1163
|
+
}
|
|
1164
|
+
return this.sessionPassword;
|
|
1165
|
+
}
|
|
1166
|
+
rotateSessionPassword() {
|
|
1167
|
+
this.generateNewSessionPassword();
|
|
1168
|
+
this.emit("passwordRotated");
|
|
1169
|
+
return this.sessionPassword;
|
|
1170
|
+
}
|
|
1171
|
+
get2FAState() {
|
|
1172
|
+
this.loadTotpState();
|
|
1173
|
+
logger_default.info(`[auth-state] get2FAState() returning: ${this.totpState ? `verified=${this.totpState.verified}` : "null"}`);
|
|
1174
|
+
return this.totpState;
|
|
1175
|
+
}
|
|
1176
|
+
save2FAState(state) {
|
|
1177
|
+
this.totpState = state;
|
|
1178
|
+
try {
|
|
1179
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
1180
|
+
writeFileSync2(TOTP_STATE_PATH, JSON.stringify(state, null, 2), { mode: 384 });
|
|
1181
|
+
this.emit("2faChanged");
|
|
1182
|
+
logger_default.info(`[auth-state] TOTP state saved`);
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
logger_default.error(`[auth-state] Failed to save TOTP state: ${err instanceof Error ? err.message : err}`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
clear2FAState() {
|
|
1188
|
+
this.totpState = null;
|
|
1189
|
+
try {
|
|
1190
|
+
if (existsSync2(TOTP_STATE_PATH)) {
|
|
1191
|
+
unlinkSync(TOTP_STATE_PATH);
|
|
1192
|
+
}
|
|
1193
|
+
this.emit("2faChanged");
|
|
1194
|
+
logger_default.info(`[auth-state] TOTP state cleared`);
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
logger_default.error(`[auth-state] Failed to clear TOTP state: ${err instanceof Error ? err.message : err}`);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
on2FAStateChange(callback) {
|
|
1200
|
+
this.on("2faChanged", callback);
|
|
1201
|
+
}
|
|
1202
|
+
onPasswordRotated(callback) {
|
|
1203
|
+
this.on("passwordRotated", callback);
|
|
1204
|
+
}
|
|
1205
|
+
cleanup() {
|
|
1206
|
+
this.watchers.forEach((w) => clearTimeout(w));
|
|
1207
|
+
this.removeAllListeners();
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
authState = new AuthState();
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
// src/server/passkey-state.ts
|
|
1215
|
+
var passkey_state_exports = {};
|
|
1216
|
+
__export(passkey_state_exports, {
|
|
1217
|
+
clearPasskeys: () => clearPasskeys,
|
|
1218
|
+
deletePasskey: () => deletePasskey,
|
|
1219
|
+
getPasskeyById: () => getPasskeyById,
|
|
1220
|
+
getPasskeys: () => getPasskeys,
|
|
1221
|
+
savePasskey: () => savePasskey
|
|
1222
|
+
});
|
|
1223
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1224
|
+
import { join as join3 } from "node:path";
|
|
1225
|
+
import { homedir as homedir3 } from "node:os";
|
|
1226
|
+
function loadPasskeys() {
|
|
1227
|
+
if (existsSync3(PASSKEYS_PATH)) {
|
|
1228
|
+
try {
|
|
1229
|
+
const data = readFileSync3(PASSKEYS_PATH, "utf-8");
|
|
1230
|
+
const passkeys = JSON.parse(data);
|
|
1231
|
+
logger_default.info(`[passkey-state] Loaded ${passkeys.length} passkeys from disk`);
|
|
1232
|
+
return passkeys;
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
logger_default.error(`[passkey-state] Failed to load passkeys: ${err instanceof Error ? err.message : err}`);
|
|
1235
|
+
return [];
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return [];
|
|
1239
|
+
}
|
|
1240
|
+
function savePasskeys(passkeys) {
|
|
1241
|
+
try {
|
|
1242
|
+
mkdirSync2(STATE_DIR2, { recursive: true });
|
|
1243
|
+
writeFileSync3(PASSKEYS_PATH, JSON.stringify(passkeys, null, 2), { mode: 384 });
|
|
1244
|
+
logger_default.info(`[passkey-state] Saved ${passkeys.length} passkeys to disk`);
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
logger_default.error(`[passkey-state] Failed to save passkeys: ${err instanceof Error ? err.message : err}`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
function getPasskeys() {
|
|
1250
|
+
return loadPasskeys();
|
|
1251
|
+
}
|
|
1252
|
+
function savePasskey(cred) {
|
|
1253
|
+
const passkeys = loadPasskeys();
|
|
1254
|
+
const existingIndex = passkeys.findIndex((p) => p.id === cred.id);
|
|
1255
|
+
if (existingIndex >= 0) {
|
|
1256
|
+
passkeys[existingIndex] = cred;
|
|
1257
|
+
} else {
|
|
1258
|
+
passkeys.push(cred);
|
|
1259
|
+
}
|
|
1260
|
+
savePasskeys(passkeys);
|
|
1261
|
+
}
|
|
1262
|
+
function deletePasskey(id) {
|
|
1263
|
+
const passkeys = loadPasskeys();
|
|
1264
|
+
const filtered = passkeys.filter((p) => p.id !== id);
|
|
1265
|
+
savePasskeys(filtered);
|
|
1266
|
+
logger_default.info(`[passkey-state] Deleted passkey ${id.slice(0, 8)}...`);
|
|
1267
|
+
}
|
|
1268
|
+
function getPasskeyById(id) {
|
|
1269
|
+
const passkeys = loadPasskeys();
|
|
1270
|
+
return passkeys.find((p) => p.id === id) ?? null;
|
|
1271
|
+
}
|
|
1272
|
+
function clearPasskeys() {
|
|
1273
|
+
try {
|
|
1274
|
+
if (existsSync3(PASSKEYS_PATH)) {
|
|
1275
|
+
unlinkSync2(PASSKEYS_PATH);
|
|
1276
|
+
}
|
|
1277
|
+
logger_default.info(`[passkey-state] All passkeys cleared`);
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
logger_default.error(`[passkey-state] Failed to clear passkeys: ${err instanceof Error ? err.message : err}`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
var STATE_DIR2, PASSKEYS_PATH;
|
|
1283
|
+
var init_passkey_state = __esm({
|
|
1284
|
+
"src/server/passkey-state.ts"() {
|
|
1285
|
+
init_logger();
|
|
1286
|
+
STATE_DIR2 = join3(homedir3(), ".puttry");
|
|
1287
|
+
PASSKEYS_PATH = join3(STATE_DIR2, "passkeys.json");
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
// src/server/cli.ts
|
|
1292
|
+
import { spawn } from "node:child_process";
|
|
1293
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "node:fs";
|
|
1294
|
+
import { join as join4 } from "node:path";
|
|
1295
|
+
import { homedir as homedir4 } from "node:os";
|
|
1296
|
+
import { setTimeout as setTimeoutPromise } from "node:timers/promises";
|
|
1297
|
+
var PID_DIR = join4(homedir4(), ".puttry");
|
|
1298
|
+
var PID_PATH = join4(PID_DIR, "server.pid");
|
|
1299
|
+
function loadEnvFiles() {
|
|
1300
|
+
const envPaths = [
|
|
1301
|
+
join4(import.meta.dirname, "../../.env.local"),
|
|
1302
|
+
join4(homedir4(), ".puttry", ".env")
|
|
1303
|
+
];
|
|
1304
|
+
for (const envPath of envPaths) {
|
|
1305
|
+
if (existsSync4(envPath)) {
|
|
1306
|
+
try {
|
|
1307
|
+
const envContent = readFileSync4(envPath, "utf-8");
|
|
1308
|
+
const lines = envContent.split("\n");
|
|
1309
|
+
for (const line of lines) {
|
|
1310
|
+
const trimmed = line.trim();
|
|
1311
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
1312
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
1313
|
+
const value = valueParts.join("=");
|
|
1314
|
+
if (key && !process.env[key]) {
|
|
1315
|
+
process.env[key] = value;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function printHelp() {
|
|
1325
|
+
console.log(`
|
|
1326
|
+
Usage: puttry <command>
|
|
1327
|
+
|
|
1328
|
+
Commands:
|
|
1329
|
+
start Start the server in the background
|
|
1330
|
+
stop Stop the running server
|
|
1331
|
+
restart Restart the server
|
|
1332
|
+
status Show server status
|
|
1333
|
+
|
|
1334
|
+
password Show the current session password
|
|
1335
|
+
password show Show the current session password
|
|
1336
|
+
password rotate Rotate to a new session password
|
|
1337
|
+
|
|
1338
|
+
totp reset Clear TOTP configuration
|
|
1339
|
+
passkey list List registered passkeys
|
|
1340
|
+
passkey reset Clear all registered passkeys
|
|
1341
|
+
|
|
1342
|
+
configure Interactive configuration wizard
|
|
1343
|
+
config list List all configuration values
|
|
1344
|
+
config set KEY VAL Update a configuration value
|
|
1345
|
+
|
|
1346
|
+
help Show this help message
|
|
1347
|
+
`);
|
|
1348
|
+
}
|
|
1349
|
+
async function startServer() {
|
|
1350
|
+
const serverPath = join4(import.meta.dirname, "server.js");
|
|
1351
|
+
if (!existsSync4(serverPath)) {
|
|
1352
|
+
console.error(`Error: Server not found at ${serverPath}`);
|
|
1353
|
+
console.error("Run 'npm run build:server' to compile the server.");
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
}
|
|
1356
|
+
if (existsSync4(PID_PATH)) {
|
|
1357
|
+
const pidContent = readFileSync4(PID_PATH, "utf-8").trim();
|
|
1358
|
+
const pid = parseInt(pidContent);
|
|
1359
|
+
try {
|
|
1360
|
+
process.kill(pid, 0);
|
|
1361
|
+
console.log(`Server is already running (PID: ${pid})`);
|
|
1362
|
+
return;
|
|
1363
|
+
} catch {
|
|
1364
|
+
try {
|
|
1365
|
+
unlinkSync3(PID_PATH);
|
|
1366
|
+
} catch {
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
1371
|
+
detached: true,
|
|
1372
|
+
stdio: "ignore",
|
|
1373
|
+
env: { ...process.env }
|
|
1374
|
+
});
|
|
1375
|
+
child.unref();
|
|
1376
|
+
await setTimeoutPromise(1e3);
|
|
1377
|
+
const passwordPath = join4(PID_DIR, "session-password.txt");
|
|
1378
|
+
let attempts = 0;
|
|
1379
|
+
while (attempts < 10) {
|
|
1380
|
+
if (existsSync4(passwordPath)) {
|
|
1381
|
+
try {
|
|
1382
|
+
const password = readFileSync4(passwordPath, "utf-8").trim();
|
|
1383
|
+
const port = process.env.PORT || "5174";
|
|
1384
|
+
const host = process.env.HOST || "localhost";
|
|
1385
|
+
console.log("");
|
|
1386
|
+
console.log("\u2713 Server started successfully");
|
|
1387
|
+
console.log("");
|
|
1388
|
+
console.log("\u2500\u2500\u2500 Session Password \u2500\u2500\u2500");
|
|
1389
|
+
console.log(password);
|
|
1390
|
+
console.log("");
|
|
1391
|
+
console.log("\u2500\u2500\u2500 Direct Link \u2500\u2500\u2500");
|
|
1392
|
+
console.log(`http://${host}:${port}/`);
|
|
1393
|
+
console.log("");
|
|
1394
|
+
const envLocal = join4(import.meta.dirname, "../../.env.local");
|
|
1395
|
+
const envUser = join4(homedir4(), ".puttry", ".env");
|
|
1396
|
+
const isFirstRun = !existsSync4(envLocal) && !existsSync4(envUser);
|
|
1397
|
+
if (isFirstRun) {
|
|
1398
|
+
console.log("\u2139 Running with default settings. Use 'puttry config list' to view or 'puttry config set KEY VALUE' to change.");
|
|
1399
|
+
console.log("");
|
|
1400
|
+
}
|
|
1401
|
+
return;
|
|
1402
|
+
} catch {
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
await setTimeoutPromise(100);
|
|
1406
|
+
attempts++;
|
|
1407
|
+
}
|
|
1408
|
+
console.log("\u2713 Server started (password file not yet available)");
|
|
1409
|
+
}
|
|
1410
|
+
async function stopServer() {
|
|
1411
|
+
if (!existsSync4(PID_PATH)) {
|
|
1412
|
+
console.log("Server is not running");
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
try {
|
|
1416
|
+
const pidContent = readFileSync4(PID_PATH, "utf-8").trim();
|
|
1417
|
+
const pid = parseInt(pidContent);
|
|
1418
|
+
try {
|
|
1419
|
+
process.kill(pid, 0);
|
|
1420
|
+
} catch {
|
|
1421
|
+
console.log("Server is not running (stale PID file)");
|
|
1422
|
+
unlinkSync3(PID_PATH);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
process.kill(pid, "SIGTERM");
|
|
1426
|
+
try {
|
|
1427
|
+
unlinkSync3(PID_PATH);
|
|
1428
|
+
} catch {
|
|
1429
|
+
}
|
|
1430
|
+
console.log(`Stopped server (PID: ${pid})`);
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
console.error(`Failed to stop server: ${err instanceof Error ? err.message : err}`);
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
async function restartServer() {
|
|
1437
|
+
if (existsSync4(PID_PATH)) {
|
|
1438
|
+
await stopServer();
|
|
1439
|
+
await setTimeoutPromise(500);
|
|
1440
|
+
}
|
|
1441
|
+
await startServer();
|
|
1442
|
+
}
|
|
1443
|
+
async function statusServer() {
|
|
1444
|
+
if (!existsSync4(PID_PATH)) {
|
|
1445
|
+
console.log("Server is stopped");
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
try {
|
|
1449
|
+
const pidContent = readFileSync4(PID_PATH, "utf-8").trim();
|
|
1450
|
+
const pid = parseInt(pidContent);
|
|
1451
|
+
try {
|
|
1452
|
+
process.kill(pid, 0);
|
|
1453
|
+
const port = process.env.PORT || "5174";
|
|
1454
|
+
console.log(`Server is running (PID: ${pid}, http://localhost:${port})`);
|
|
1455
|
+
return;
|
|
1456
|
+
} catch {
|
|
1457
|
+
console.log("Server is stopped (stale PID file)");
|
|
1458
|
+
unlinkSync3(PID_PATH);
|
|
1459
|
+
}
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
console.error(`Failed to check status: ${err instanceof Error ? err.message : err}`);
|
|
1462
|
+
process.exit(1);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
async function showPassword() {
|
|
1466
|
+
loadEnvFiles();
|
|
1467
|
+
const { initializeConfig: initializeConfig2 } = await Promise.resolve().then(() => (init_settings_api(), settings_api_exports));
|
|
1468
|
+
const { initAuthState: initAuthState2, getSessionPassword: getSessionPassword2 } = await Promise.resolve().then(() => (init_auth_state(), auth_state_exports));
|
|
1469
|
+
initializeConfig2();
|
|
1470
|
+
await initAuthState2();
|
|
1471
|
+
const password = getSessionPassword2();
|
|
1472
|
+
const port = process.env.PORT || "5174";
|
|
1473
|
+
const host = process.env.HOST || "localhost";
|
|
1474
|
+
console.log("");
|
|
1475
|
+
console.log("\u2500\u2500\u2500 Session Password \u2500\u2500\u2500");
|
|
1476
|
+
console.log(password);
|
|
1477
|
+
console.log("");
|
|
1478
|
+
console.log("\u2500\u2500\u2500 Direct Link \u2500\u2500\u2500");
|
|
1479
|
+
console.log(`http://${host}:${port}/`);
|
|
1480
|
+
console.log("");
|
|
1481
|
+
}
|
|
1482
|
+
async function rotatePassword() {
|
|
1483
|
+
loadEnvFiles();
|
|
1484
|
+
const { initializeConfig: initializeConfig2 } = await Promise.resolve().then(() => (init_settings_api(), settings_api_exports));
|
|
1485
|
+
const { initAuthState: initAuthState2, rotateSessionPassword: rotateSessionPassword2 } = await Promise.resolve().then(() => (init_auth_state(), auth_state_exports));
|
|
1486
|
+
initializeConfig2();
|
|
1487
|
+
await initAuthState2();
|
|
1488
|
+
const newPassword = rotateSessionPassword2();
|
|
1489
|
+
console.log("");
|
|
1490
|
+
console.log("\u2713 Password rotated successfully!");
|
|
1491
|
+
console.log("");
|
|
1492
|
+
console.log("\u2500\u2500\u2500 New Session Password \u2500\u2500\u2500");
|
|
1493
|
+
console.log(newPassword);
|
|
1494
|
+
console.log("");
|
|
1495
|
+
console.log("If the server is running, it will automatically invalidate all existing sessions.");
|
|
1496
|
+
console.log("Users will need to log in again with the new password.");
|
|
1497
|
+
console.log("");
|
|
1498
|
+
}
|
|
1499
|
+
async function listConfig() {
|
|
1500
|
+
loadEnvFiles();
|
|
1501
|
+
const { initializeConfig: initializeConfig2, config: config2 } = await Promise.resolve().then(() => (init_settings_api(), settings_api_exports));
|
|
1502
|
+
initializeConfig2();
|
|
1503
|
+
console.log("");
|
|
1504
|
+
console.log("\u2500\u2500\u2500 Configuration \u2500\u2500\u2500");
|
|
1505
|
+
for (const [key, value] of Object.entries(config2)) {
|
|
1506
|
+
console.log(`${key}=${value}`);
|
|
1507
|
+
}
|
|
1508
|
+
console.log("");
|
|
1509
|
+
}
|
|
1510
|
+
async function setConfig(key, value) {
|
|
1511
|
+
loadEnvFiles();
|
|
1512
|
+
const { initializeConfig: initializeConfig2, updateSetting: updateSetting2 } = await Promise.resolve().then(() => (init_settings_api(), settings_api_exports));
|
|
1513
|
+
initializeConfig2();
|
|
1514
|
+
const result = updateSetting2(key, value);
|
|
1515
|
+
if (!result.success) {
|
|
1516
|
+
console.error(`Error: ${result.error}`);
|
|
1517
|
+
process.exit(1);
|
|
1518
|
+
}
|
|
1519
|
+
console.log(`\u2713 Updated ${key}=${value}`);
|
|
1520
|
+
if (result.note) {
|
|
1521
|
+
console.log(` Note: ${result.note}`);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
async function runConfigureWizard() {
|
|
1525
|
+
loadEnvFiles();
|
|
1526
|
+
const { getEnvFilePath: getEnvFilePath2, parseEnvFile: parseEnvFile2, writeEnvFile: writeEnvFile2 } = await Promise.resolve().then(() => (init_settings_api(), settings_api_exports));
|
|
1527
|
+
const readline = await import("node:readline");
|
|
1528
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1529
|
+
const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
|
|
1530
|
+
const envPath = getEnvFilePath2();
|
|
1531
|
+
const current = parseEnvFile2(envPath);
|
|
1532
|
+
const changes = {};
|
|
1533
|
+
function currentDisplay(key, defaultVal) {
|
|
1534
|
+
return current[key] !== void 0 ? `${current[key]}` : `${defaultVal} (default)`;
|
|
1535
|
+
}
|
|
1536
|
+
async function promptSetting(key, description, defaultVal, type, enumValues) {
|
|
1537
|
+
const display = currentDisplay(key, defaultVal);
|
|
1538
|
+
console.log(`
|
|
1539
|
+
${key}`);
|
|
1540
|
+
console.log(` ${description}`);
|
|
1541
|
+
console.log(` Current value: ${display}`);
|
|
1542
|
+
let input = await ask("> ");
|
|
1543
|
+
if (input.trim() === "") {
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
let normalized = input.trim();
|
|
1547
|
+
if (type === "boolean") {
|
|
1548
|
+
const lower = normalized.toLowerCase();
|
|
1549
|
+
if (["true", "yes", "y", "1"].includes(lower)) {
|
|
1550
|
+
normalized = "true";
|
|
1551
|
+
} else if (["false", "no", "n", "0"].includes(lower)) {
|
|
1552
|
+
normalized = "false";
|
|
1553
|
+
} else {
|
|
1554
|
+
console.log(` Invalid boolean value. Use true/false, yes/no, y/n, or 1/0.`);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (type === "enum" && enumValues && !enumValues.includes(normalized)) {
|
|
1559
|
+
console.log(` Invalid value. Must be one of: ${enumValues.join(", ")}`);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (type === "number" && isNaN(Number(normalized))) {
|
|
1563
|
+
console.log(` Invalid number.`);
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (current[key] !== normalized) {
|
|
1567
|
+
changes[key] = normalized;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
console.log("\nWelcome to PuTTrY configuration. Press Enter to keep the current value.\n");
|
|
1571
|
+
console.log("\u2500\u2500\u2500 Network \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
1572
|
+
await promptSetting("PORT", "HTTP port the server listens on", "5174", "number");
|
|
1573
|
+
await promptSetting("HOST", "Network interface to bind to (0.0.0.0 = all interfaces, 127.0.0.1 = localhost only)", "0.0.0.0", "string");
|
|
1574
|
+
console.log("\n\u2500\u2500\u2500 Authentication \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
1575
|
+
await promptSetting(
|
|
1576
|
+
"AUTH_DISABLED",
|
|
1577
|
+
"Disable password authentication. Anyone with the URL can access the terminal. Not recommended for remote access. (true/false)",
|
|
1578
|
+
"false",
|
|
1579
|
+
"boolean"
|
|
1580
|
+
);
|
|
1581
|
+
const authDisabled = changes["AUTH_DISABLED"] === "true" || changes["AUTH_DISABLED"] === void 0 && (current["AUTH_DISABLED"] === "true" || current["AUTH_DISABLED"] === "1");
|
|
1582
|
+
await promptSetting(
|
|
1583
|
+
"SESSION_PASSWORD_TYPE",
|
|
1584
|
+
`Password style: 'xkcd' = memorable word phrase (e.g. "correct horse battery staple"), 'random' = hex string`,
|
|
1585
|
+
"xkcd",
|
|
1586
|
+
"enum",
|
|
1587
|
+
["xkcd", "random"]
|
|
1588
|
+
);
|
|
1589
|
+
await promptSetting("SESSION_PASSWORD_LENGTH", "Number of words (xkcd) or characters (random) in the session password", "4", "number");
|
|
1590
|
+
await promptSetting("LOG_SESSION_PASSWORD", "Print the session password to the console on server start (true/false)", "true", "boolean");
|
|
1591
|
+
await promptSetting(
|
|
1592
|
+
"TOTP_ENABLED",
|
|
1593
|
+
"Require a TOTP code (authenticator app / 2FA) in addition to the session password (true/false)",
|
|
1594
|
+
"false",
|
|
1595
|
+
"boolean"
|
|
1596
|
+
);
|
|
1597
|
+
await promptSetting(
|
|
1598
|
+
"PASSKEY_RP_ORIGIN",
|
|
1599
|
+
"WebAuthn passkey origin, e.g. https://example.com \u2014 required for passkey login from a custom domain",
|
|
1600
|
+
"",
|
|
1601
|
+
"string"
|
|
1602
|
+
);
|
|
1603
|
+
if (!authDisabled) {
|
|
1604
|
+
console.log("\n\u2500\u2500\u2500 Rate Limiting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
1605
|
+
await promptSetting("RATE_LIMIT_GLOBAL_MAX", "Max HTTP requests per 15 minutes per IP", "500", "number");
|
|
1606
|
+
await promptSetting("RATE_LIMIT_SESSION_PASSWORD_MAX", "Max login attempts per hour per IP", "10", "number");
|
|
1607
|
+
}
|
|
1608
|
+
console.log("\n\u2500\u2500\u2500 Terminal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
1609
|
+
await promptSetting("SCROLLBACK_LINES", "Terminal scrollback buffer \u2014 lines kept in memory per session", "10000", "number");
|
|
1610
|
+
if (Object.keys(changes).length === 0) {
|
|
1611
|
+
rl.close();
|
|
1612
|
+
console.log("\nNo changes made. Configuration unchanged.");
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
console.log("\n\u2500\u2500\u2500 Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
1616
|
+
console.log("Changes to apply:");
|
|
1617
|
+
for (const [k, v] of Object.entries(changes)) {
|
|
1618
|
+
console.log(` ${k}=${v}`);
|
|
1619
|
+
}
|
|
1620
|
+
const confirm = await ask("\nApply changes? (y/N): ");
|
|
1621
|
+
rl.close();
|
|
1622
|
+
if (confirm.trim().toLowerCase() !== "y") {
|
|
1623
|
+
console.log("Cancelled.");
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const merged = { ...current, ...changes };
|
|
1627
|
+
writeEnvFile2(envPath, merged);
|
|
1628
|
+
console.log("\n\u2713 Configuration saved.");
|
|
1629
|
+
}
|
|
1630
|
+
async function resetTotp() {
|
|
1631
|
+
loadEnvFiles();
|
|
1632
|
+
const { clear2FAState: clear2FAState2 } = await Promise.resolve().then(() => (init_auth_state(), auth_state_exports));
|
|
1633
|
+
clear2FAState2();
|
|
1634
|
+
console.log("\u2713 TOTP configuration cleared.");
|
|
1635
|
+
console.log(" Users will need to re-scan the QR code on their next login to set up 2FA again.");
|
|
1636
|
+
}
|
|
1637
|
+
async function listPasskeys() {
|
|
1638
|
+
const { getPasskeys: getPasskeys2 } = await Promise.resolve().then(() => (init_passkey_state(), passkey_state_exports));
|
|
1639
|
+
const passkeys = getPasskeys2();
|
|
1640
|
+
if (passkeys.length === 0) {
|
|
1641
|
+
console.log("No passkeys registered.");
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
console.log("");
|
|
1645
|
+
console.log("\u2500\u2500\u2500 Registered Passkeys \u2500\u2500\u2500");
|
|
1646
|
+
for (const pk of passkeys) {
|
|
1647
|
+
const registeredDate = new Date(pk.registeredAt).toLocaleDateString();
|
|
1648
|
+
console.log(` \u2022 ${pk.name} (${registeredDate})`);
|
|
1649
|
+
console.log(` ID: ${pk.id.slice(0, 16)}...`);
|
|
1650
|
+
}
|
|
1651
|
+
console.log("");
|
|
1652
|
+
}
|
|
1653
|
+
async function resetPasskeys() {
|
|
1654
|
+
const { getPasskeys: getPasskeys2, clearPasskeys: clearPasskeys2 } = await Promise.resolve().then(() => (init_passkey_state(), passkey_state_exports));
|
|
1655
|
+
const passkeys = getPasskeys2();
|
|
1656
|
+
if (passkeys.length === 0) {
|
|
1657
|
+
console.log("No passkeys registered.");
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
clearPasskeys2();
|
|
1661
|
+
console.log(`\u2713 Cleared ${passkeys.length} passkey(s).`);
|
|
1662
|
+
console.log(" Users will need to register a new passkey on their next login.");
|
|
1663
|
+
}
|
|
1664
|
+
async function main() {
|
|
1665
|
+
const args = process.argv.slice(2);
|
|
1666
|
+
if (args.length === 0) {
|
|
1667
|
+
printHelp();
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
const command = args[0];
|
|
1671
|
+
try {
|
|
1672
|
+
switch (command) {
|
|
1673
|
+
case "help":
|
|
1674
|
+
printHelp();
|
|
1675
|
+
break;
|
|
1676
|
+
case "start":
|
|
1677
|
+
await startServer();
|
|
1678
|
+
break;
|
|
1679
|
+
case "stop":
|
|
1680
|
+
await stopServer();
|
|
1681
|
+
break;
|
|
1682
|
+
case "restart":
|
|
1683
|
+
await restartServer();
|
|
1684
|
+
break;
|
|
1685
|
+
case "status":
|
|
1686
|
+
await statusServer();
|
|
1687
|
+
break;
|
|
1688
|
+
case "password":
|
|
1689
|
+
if (args[1] === "show" || args.length === 1) {
|
|
1690
|
+
await showPassword();
|
|
1691
|
+
} else if (args[1] === "rotate") {
|
|
1692
|
+
await rotatePassword();
|
|
1693
|
+
} else {
|
|
1694
|
+
console.error(`Unknown password command: ${args[1]}`);
|
|
1695
|
+
printHelp();
|
|
1696
|
+
process.exit(1);
|
|
1697
|
+
}
|
|
1698
|
+
break;
|
|
1699
|
+
case "configure":
|
|
1700
|
+
await runConfigureWizard();
|
|
1701
|
+
break;
|
|
1702
|
+
case "config":
|
|
1703
|
+
if (args[1] === "list") {
|
|
1704
|
+
await listConfig();
|
|
1705
|
+
} else if (args[1] === "set") {
|
|
1706
|
+
if (args.length < 4) {
|
|
1707
|
+
console.error("Usage: puttry config set KEY VALUE");
|
|
1708
|
+
process.exit(1);
|
|
1709
|
+
}
|
|
1710
|
+
await setConfig(args[2], args[3]);
|
|
1711
|
+
} else {
|
|
1712
|
+
console.error(`Unknown config command: ${args[1]}`);
|
|
1713
|
+
printHelp();
|
|
1714
|
+
process.exit(1);
|
|
1715
|
+
}
|
|
1716
|
+
break;
|
|
1717
|
+
case "totp":
|
|
1718
|
+
if (args[1] === "reset") {
|
|
1719
|
+
await resetTotp();
|
|
1720
|
+
} else {
|
|
1721
|
+
console.error(`Unknown totp command: ${args[1]}`);
|
|
1722
|
+
printHelp();
|
|
1723
|
+
process.exit(1);
|
|
1724
|
+
}
|
|
1725
|
+
break;
|
|
1726
|
+
case "passkey":
|
|
1727
|
+
if (args[1] === "list") {
|
|
1728
|
+
await listPasskeys();
|
|
1729
|
+
} else if (args[1] === "reset") {
|
|
1730
|
+
await resetPasskeys();
|
|
1731
|
+
} else {
|
|
1732
|
+
console.error(`Unknown passkey command: ${args[1]}`);
|
|
1733
|
+
printHelp();
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
break;
|
|
1737
|
+
default:
|
|
1738
|
+
console.error(`Unknown command: ${command}`);
|
|
1739
|
+
printHelp();
|
|
1740
|
+
process.exit(1);
|
|
1741
|
+
}
|
|
1742
|
+
} catch (err) {
|
|
1743
|
+
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
1744
|
+
process.exit(1);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
main().catch((err) => {
|
|
1748
|
+
console.error(`Fatal error: ${err instanceof Error ? err.message : err}`);
|
|
1749
|
+
process.exit(1);
|
|
1750
|
+
});
|