@neuralnomads/codenomad-dev 0.10.3-dev-20260213-ba418a85 → 0.10.3-dev-20260213-e9f281a6
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/package.json +1 -1
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/assets/{main-CSlDZj4f.js → main-crtt5pqm.js} +82 -80
- package/public/index.html +1 -1
- package/public/sw.js +1 -1
- package/public/ui-version.json +1 -1
- package/dist/integrations/github/bot-signature.js +0 -11
- package/dist/integrations/github/git-ops.js +0 -133
- package/dist/integrations/github/github-types.js +0 -1
- package/dist/integrations/github/job-runner.js +0 -608
- package/dist/integrations/github/octokit.js +0 -58
- package/dist/integrations/github/sanitize-webhook.js +0 -42
- package/dist/integrations/github/webhook-verify.js +0 -21
- package/dist/integrations/github/workspace-context.js +0 -10
- package/dist/integrations/github/worktree-context.js +0 -15
- package/dist/opencode/request-context.js +0 -39
- package/dist/opencode/worktree-directory.js +0 -42
- package/dist/opencode-config-template/README.md +0 -32
- package/dist/opencode-config-template/opencode.jsonc +0 -3
- package/dist/opencode-config-template/plugin/codenomad.ts +0 -40
- package/dist/opencode-config-template/plugin/lib/background-process.ts +0 -160
- package/dist/opencode-config-template/plugin/lib/client.ts +0 -165
- package/dist/server/routes/github-plugin.js +0 -215
- package/dist/server/routes/github-webhook.js +0 -32
- package/scripts/copy-auth-pages.mjs +0 -22
- package/scripts/copy-opencode-config.mjs +0 -61
- package/scripts/copy-ui-dist.mjs +0 -21
- package/src/api-types.ts +0 -326
- package/src/auth/auth-store.ts +0 -175
- package/src/auth/http-auth.ts +0 -38
- package/src/auth/manager.ts +0 -163
- package/src/auth/password-hash.ts +0 -49
- package/src/auth/session-manager.ts +0 -23
- package/src/auth/token-manager.ts +0 -32
- package/src/background-processes/manager.ts +0 -519
- package/src/bin.ts +0 -29
- package/src/config/binaries.ts +0 -192
- package/src/config/location.ts +0 -78
- package/src/config/schema.ts +0 -104
- package/src/config/store.ts +0 -244
- package/src/events/bus.ts +0 -45
- package/src/filesystem/__tests__/search-cache.test.ts +0 -61
- package/src/filesystem/browser.ts +0 -353
- package/src/filesystem/search-cache.ts +0 -66
- package/src/filesystem/search.ts +0 -184
- package/src/index.ts +0 -540
- package/src/launcher.ts +0 -177
- package/src/loader.ts +0 -21
- package/src/logger.ts +0 -133
- package/src/opencode-config.ts +0 -31
- package/src/plugins/channel.ts +0 -55
- package/src/plugins/handlers.ts +0 -36
- package/src/releases/dev-release-monitor.ts +0 -118
- package/src/releases/release-monitor.ts +0 -149
- package/src/server/http-server.ts +0 -693
- package/src/server/network-addresses.ts +0 -75
- package/src/server/routes/auth-pages/login.html +0 -134
- package/src/server/routes/auth-pages/token.html +0 -93
- package/src/server/routes/auth.ts +0 -164
- package/src/server/routes/background-processes.ts +0 -85
- package/src/server/routes/config.ts +0 -76
- package/src/server/routes/events.ts +0 -61
- package/src/server/routes/filesystem.ts +0 -54
- package/src/server/routes/meta.ts +0 -58
- package/src/server/routes/plugin.ts +0 -75
- package/src/server/routes/storage.ts +0 -66
- package/src/server/routes/workspaces.ts +0 -113
- package/src/server/routes/worktrees.ts +0 -195
- package/src/server/tls.ts +0 -283
- package/src/storage/instance-store.ts +0 -64
- package/src/ui/__tests__/remote-ui.test.ts +0 -58
- package/src/ui/remote-ui.ts +0 -571
- package/src/workspaces/git-worktrees.ts +0 -241
- package/src/workspaces/instance-events.ts +0 -226
- package/src/workspaces/manager.ts +0 -493
- package/src/workspaces/opencode-auth.ts +0 -22
- package/src/workspaces/runtime.ts +0 -428
- package/src/workspaces/worktree-map.ts +0 -129
- package/tsconfig.json +0 -17
package/src/auth/auth-store.ts
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import fs from "fs"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import type { Logger } from "../logger"
|
|
4
|
-
import { hashPassword, type PasswordHashRecord, verifyPassword } from "./password-hash"
|
|
5
|
-
|
|
6
|
-
export interface AuthFile {
|
|
7
|
-
version: 1
|
|
8
|
-
username: string
|
|
9
|
-
password: PasswordHashRecord
|
|
10
|
-
userProvided: boolean
|
|
11
|
-
updatedAt: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface AuthStatus {
|
|
15
|
-
username: string
|
|
16
|
-
passwordUserProvided: boolean
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class AuthStore {
|
|
20
|
-
private cachedFile: AuthFile | null = null
|
|
21
|
-
private overrideAuth: AuthFile | null = null
|
|
22
|
-
private bootstrapUsername: string | null = null
|
|
23
|
-
|
|
24
|
-
constructor(private readonly authFilePath: string, private readonly logger: Logger) {}
|
|
25
|
-
|
|
26
|
-
getAuthFilePath() {
|
|
27
|
-
return this.authFilePath
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
load(): AuthFile | null {
|
|
31
|
-
if (this.overrideAuth) {
|
|
32
|
-
return this.overrideAuth
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (this.cachedFile) {
|
|
36
|
-
return this.cachedFile
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
if (!fs.existsSync(this.authFilePath)) {
|
|
41
|
-
return null
|
|
42
|
-
}
|
|
43
|
-
const raw = fs.readFileSync(this.authFilePath, "utf-8")
|
|
44
|
-
const parsed = JSON.parse(raw) as AuthFile
|
|
45
|
-
if (!parsed || parsed.version !== 1) {
|
|
46
|
-
this.logger.warn({ authFilePath: this.authFilePath }, "Auth file has unsupported version")
|
|
47
|
-
return null
|
|
48
|
-
}
|
|
49
|
-
this.cachedFile = parsed
|
|
50
|
-
return parsed
|
|
51
|
-
} catch (error) {
|
|
52
|
-
this.logger.warn({ err: error, authFilePath: this.authFilePath }, "Failed to load auth file")
|
|
53
|
-
return null
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
ensureInitialized(params: {
|
|
58
|
-
username: string
|
|
59
|
-
password?: string
|
|
60
|
-
allowBootstrapWithoutPassword: boolean
|
|
61
|
-
}): void {
|
|
62
|
-
const password = params.password?.trim()
|
|
63
|
-
if (password) {
|
|
64
|
-
const now = new Date().toISOString()
|
|
65
|
-
const runtime: AuthFile = {
|
|
66
|
-
version: 1,
|
|
67
|
-
username: params.username,
|
|
68
|
-
password: hashPassword(password),
|
|
69
|
-
userProvided: true,
|
|
70
|
-
updatedAt: now,
|
|
71
|
-
}
|
|
72
|
-
this.overrideAuth = runtime
|
|
73
|
-
this.cachedFile = null
|
|
74
|
-
this.bootstrapUsername = null
|
|
75
|
-
this.logger.debug({ authFilePath: this.authFilePath }, "Using runtime auth password override; ignoring auth file")
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const existing = this.load()
|
|
80
|
-
if (existing) {
|
|
81
|
-
if (existing.username !== params.username) {
|
|
82
|
-
// Keep existing username unless explicitly overridden later.
|
|
83
|
-
this.logger.debug({ existing: existing.username, requested: params.username }, "Auth username differs from requested")
|
|
84
|
-
}
|
|
85
|
-
this.bootstrapUsername = null
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (params.allowBootstrapWithoutPassword) {
|
|
90
|
-
this.bootstrapUsername = params.username
|
|
91
|
-
this.logger.debug({ authFilePath: this.authFilePath }, "No auth file present; bootstrap-only mode enabled")
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
throw new Error(
|
|
96
|
-
`No server password configured. Create ${this.authFilePath} or start with --password / CODENOMAD_SERVER_PASSWORD.`,
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
validateCredentials(username: string, password: string): boolean {
|
|
101
|
-
const auth = this.load()
|
|
102
|
-
if (!auth) {
|
|
103
|
-
return false
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (username !== auth.username) {
|
|
107
|
-
return false
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return verifyPassword(password, auth.password)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
setPassword(params: { password: string; markUserProvided: boolean }): AuthStatus {
|
|
114
|
-
if (this.overrideAuth) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
"Server password is provided via CLI/env and cannot be changed while running. Restart without --password / CODENOMAD_SERVER_PASSWORD to use auth.json.",
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const current = this.load()
|
|
121
|
-
|
|
122
|
-
if (!current) {
|
|
123
|
-
if (!this.bootstrapUsername) {
|
|
124
|
-
throw new Error("Auth is not initialized")
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const created: AuthFile = {
|
|
128
|
-
version: 1,
|
|
129
|
-
username: this.bootstrapUsername,
|
|
130
|
-
password: hashPassword(params.password),
|
|
131
|
-
userProvided: params.markUserProvided,
|
|
132
|
-
updatedAt: new Date().toISOString(),
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
this.persist(created)
|
|
136
|
-
this.bootstrapUsername = null
|
|
137
|
-
return { username: created.username, passwordUserProvided: created.userProvided }
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const next: AuthFile = {
|
|
141
|
-
...current,
|
|
142
|
-
password: hashPassword(params.password),
|
|
143
|
-
userProvided: params.markUserProvided,
|
|
144
|
-
updatedAt: new Date().toISOString(),
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
this.persist(next)
|
|
148
|
-
return { username: next.username, passwordUserProvided: next.userProvided }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
getStatus(): AuthStatus {
|
|
152
|
-
const current = this.load()
|
|
153
|
-
if (current) {
|
|
154
|
-
return { username: current.username, passwordUserProvided: current.userProvided }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (this.bootstrapUsername) {
|
|
158
|
-
return { username: this.bootstrapUsername, passwordUserProvided: false }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
throw new Error("Auth is not initialized")
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private persist(auth: AuthFile) {
|
|
165
|
-
try {
|
|
166
|
-
fs.mkdirSync(path.dirname(this.authFilePath), { recursive: true })
|
|
167
|
-
fs.writeFileSync(this.authFilePath, JSON.stringify(auth, null, 2), "utf-8")
|
|
168
|
-
this.cachedFile = auth
|
|
169
|
-
this.logger.debug({ authFilePath: this.authFilePath }, "Persisted auth file")
|
|
170
|
-
} catch (error) {
|
|
171
|
-
this.logger.error({ err: error, authFilePath: this.authFilePath }, "Failed to persist auth file")
|
|
172
|
-
throw error
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
package/src/auth/http-auth.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { FastifyReply, FastifyRequest } from "fastify"
|
|
2
|
-
|
|
3
|
-
export function parseCookies(header: string | undefined): Record<string, string> {
|
|
4
|
-
const result: Record<string, string> = {}
|
|
5
|
-
if (!header) return result
|
|
6
|
-
|
|
7
|
-
const parts = header.split(";")
|
|
8
|
-
for (const part of parts) {
|
|
9
|
-
const index = part.indexOf("=")
|
|
10
|
-
if (index < 0) continue
|
|
11
|
-
const key = part.slice(0, index).trim()
|
|
12
|
-
const value = part.slice(index + 1).trim()
|
|
13
|
-
if (!key) continue
|
|
14
|
-
result[key] = decodeURIComponent(value)
|
|
15
|
-
}
|
|
16
|
-
return result
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isLoopbackAddress(remoteAddress: string | undefined): boolean {
|
|
20
|
-
if (!remoteAddress) return false
|
|
21
|
-
if (remoteAddress === "127.0.0.1" || remoteAddress === "::1") return true
|
|
22
|
-
if (remoteAddress === "::ffff:127.0.0.1") return true
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function wantsHtml(request: FastifyRequest): boolean {
|
|
27
|
-
const accept = (request.headers["accept"] ?? "").toString().toLowerCase()
|
|
28
|
-
return accept.includes("text/html") || accept.includes("application/xhtml")
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function sendUnauthorized(request: FastifyRequest, reply: FastifyReply) {
|
|
32
|
-
if (request.method === "GET" && !request.url.startsWith("/api/") && wantsHtml(request)) {
|
|
33
|
-
reply.redirect("/login")
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
reply.code(401).send({ error: "Unauthorized" })
|
|
38
|
-
}
|
package/src/auth/manager.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import type { FastifyReply, FastifyRequest } from "fastify"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import type { Logger } from "../logger"
|
|
4
|
-
import { AuthStore } from "./auth-store"
|
|
5
|
-
import { TokenManager } from "./token-manager"
|
|
6
|
-
import { SessionManager } from "./session-manager"
|
|
7
|
-
import { isLoopbackAddress, parseCookies } from "./http-auth"
|
|
8
|
-
|
|
9
|
-
export const BOOTSTRAP_TOKEN_STDOUT_PREFIX = "CODENOMAD_BOOTSTRAP_TOKEN:" as const
|
|
10
|
-
export const DEFAULT_AUTH_USERNAME = "codenomad" as const
|
|
11
|
-
export const DEFAULT_AUTH_COOKIE_NAME = "codenomad_session" as const
|
|
12
|
-
|
|
13
|
-
export interface AuthManagerInit {
|
|
14
|
-
configPath: string
|
|
15
|
-
username: string
|
|
16
|
-
password?: string
|
|
17
|
-
generateToken: boolean
|
|
18
|
-
dangerouslySkipAuth?: boolean
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class AuthManager {
|
|
22
|
-
private readonly authStore: AuthStore | null
|
|
23
|
-
private readonly tokenManager: TokenManager | null
|
|
24
|
-
private readonly sessionManager = new SessionManager()
|
|
25
|
-
private readonly cookieName = DEFAULT_AUTH_COOKIE_NAME
|
|
26
|
-
private readonly authEnabled: boolean
|
|
27
|
-
|
|
28
|
-
constructor(private readonly init: AuthManagerInit, private readonly logger: Logger) {
|
|
29
|
-
this.authEnabled = !Boolean(init.dangerouslySkipAuth)
|
|
30
|
-
|
|
31
|
-
if (!this.authEnabled) {
|
|
32
|
-
this.authStore = null
|
|
33
|
-
this.tokenManager = null
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const authFilePath = resolveAuthFilePath(init.configPath)
|
|
38
|
-
this.authStore = new AuthStore(authFilePath, logger.child({ component: "auth" }))
|
|
39
|
-
|
|
40
|
-
// Startup: password comes from CLI/env, auth.json, or bootstrap-only mode.
|
|
41
|
-
this.authStore.ensureInitialized({
|
|
42
|
-
username: init.username,
|
|
43
|
-
password: init.password,
|
|
44
|
-
allowBootstrapWithoutPassword: init.generateToken,
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
this.tokenManager = init.generateToken ? new TokenManager(60_000) : null
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
isAuthEnabled(): boolean {
|
|
51
|
-
return this.authEnabled
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
getCookieName(): string {
|
|
55
|
-
return this.cookieName
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
isTokenBootstrapEnabled(): boolean {
|
|
59
|
-
return Boolean(this.tokenManager)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
issueBootstrapToken(): string | null {
|
|
63
|
-
if (!this.tokenManager) return null
|
|
64
|
-
return this.tokenManager.generate()
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
consumeBootstrapToken(token: string): boolean {
|
|
68
|
-
if (!this.tokenManager) return false
|
|
69
|
-
return this.tokenManager.consume(token)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
validateLogin(username: string, password: string): boolean {
|
|
73
|
-
if (!this.authEnabled) {
|
|
74
|
-
return true
|
|
75
|
-
}
|
|
76
|
-
return this.requireAuthStore().validateCredentials(username, password)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
createSession(username: string) {
|
|
80
|
-
if (!this.authEnabled) {
|
|
81
|
-
return { id: "auth-disabled", createdAt: Date.now(), username: this.init.username }
|
|
82
|
-
}
|
|
83
|
-
return this.sessionManager.createSession(username)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
getStatus() {
|
|
87
|
-
if (!this.authEnabled) {
|
|
88
|
-
return { username: this.init.username, passwordUserProvided: false }
|
|
89
|
-
}
|
|
90
|
-
return this.requireAuthStore().getStatus()
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
setPassword(password: string) {
|
|
94
|
-
if (!this.authEnabled) {
|
|
95
|
-
throw new Error("Internal authentication is disabled")
|
|
96
|
-
}
|
|
97
|
-
return this.requireAuthStore().setPassword({ password, markUserProvided: true })
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
isLoopbackRequest(request: FastifyRequest): boolean {
|
|
101
|
-
return isLoopbackAddress(request.socket.remoteAddress)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
getSessionFromRequest(request: FastifyRequest): { username: string; sessionId: string } | null {
|
|
105
|
-
if (!this.authEnabled) {
|
|
106
|
-
// When auth is disabled, treat all requests as authenticated.
|
|
107
|
-
// We still return a stable username so callers can display it.
|
|
108
|
-
return { username: this.init.username, sessionId: "auth-disabled" }
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const cookies = parseCookies(request.headers.cookie)
|
|
112
|
-
const sessionId = cookies[this.cookieName]
|
|
113
|
-
const session = this.sessionManager.getSession(sessionId)
|
|
114
|
-
if (!session) return null
|
|
115
|
-
return { username: session.username, sessionId: session.id }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
setSessionCookie(reply: FastifyReply, sessionId: string) {
|
|
119
|
-
reply.header("Set-Cookie", buildSessionCookie(this.cookieName, sessionId))
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
setSessionCookieWithOptions(reply: FastifyReply, sessionId: string, options?: { secure?: boolean }) {
|
|
123
|
-
reply.header("Set-Cookie", buildSessionCookie(this.cookieName, sessionId, options))
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
clearSessionCookie(reply: FastifyReply) {
|
|
127
|
-
reply.header("Set-Cookie", buildSessionCookie(this.cookieName, "", { maxAgeSeconds: 0 }))
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
clearSessionCookieWithOptions(reply: FastifyReply, options?: { secure?: boolean }) {
|
|
131
|
-
reply.header("Set-Cookie", buildSessionCookie(this.cookieName, "", { maxAgeSeconds: 0, ...options }))
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
private requireAuthStore(): AuthStore {
|
|
135
|
-
if (!this.authStore) {
|
|
136
|
-
throw new Error("Auth store is unavailable")
|
|
137
|
-
}
|
|
138
|
-
return this.authStore
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function resolveAuthFilePath(configPath: string) {
|
|
143
|
-
const resolvedConfigPath = resolvePath(configPath)
|
|
144
|
-
return path.join(path.dirname(resolvedConfigPath), "auth.json")
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function resolvePath(filePath: string) {
|
|
148
|
-
if (filePath.startsWith("~/")) {
|
|
149
|
-
return path.join(process.env.HOME ?? "", filePath.slice(2))
|
|
150
|
-
}
|
|
151
|
-
return path.resolve(filePath)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function buildSessionCookie(name: string, value: string, options?: { maxAgeSeconds?: number; secure?: boolean }) {
|
|
155
|
-
const parts = [`${name}=${encodeURIComponent(value)}`, "HttpOnly", "Path=/", "SameSite=Lax"]
|
|
156
|
-
if (options?.secure) {
|
|
157
|
-
parts.push("Secure")
|
|
158
|
-
}
|
|
159
|
-
if (options?.maxAgeSeconds !== undefined) {
|
|
160
|
-
parts.push(`Max-Age=${Math.max(0, Math.floor(options.maxAgeSeconds))}`)
|
|
161
|
-
}
|
|
162
|
-
return parts.join("; ")
|
|
163
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import crypto from "crypto"
|
|
2
|
-
|
|
3
|
-
export interface PasswordHashRecord {
|
|
4
|
-
algorithm: "scrypt"
|
|
5
|
-
saltBase64: string
|
|
6
|
-
hashBase64: string
|
|
7
|
-
keyLength: number
|
|
8
|
-
params: {
|
|
9
|
-
N: number
|
|
10
|
-
r: number
|
|
11
|
-
p: number
|
|
12
|
-
maxmem: number
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const DEFAULT_SCRYPT_PARAMS = {
|
|
17
|
-
N: 16384,
|
|
18
|
-
r: 8,
|
|
19
|
-
p: 1,
|
|
20
|
-
maxmem: 32 * 1024 * 1024,
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function hashPassword(password: string): PasswordHashRecord {
|
|
24
|
-
const salt = crypto.randomBytes(16)
|
|
25
|
-
const params = DEFAULT_SCRYPT_PARAMS
|
|
26
|
-
const keyLength = 64
|
|
27
|
-
const derived = crypto.scryptSync(password, salt, keyLength, params)
|
|
28
|
-
return {
|
|
29
|
-
algorithm: "scrypt",
|
|
30
|
-
saltBase64: salt.toString("base64"),
|
|
31
|
-
hashBase64: Buffer.from(derived).toString("base64"),
|
|
32
|
-
keyLength,
|
|
33
|
-
params,
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function verifyPassword(password: string, record: PasswordHashRecord): boolean {
|
|
38
|
-
if (record.algorithm !== "scrypt") {
|
|
39
|
-
return false
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const salt = Buffer.from(record.saltBase64, "base64")
|
|
43
|
-
const expected = Buffer.from(record.hashBase64, "base64")
|
|
44
|
-
const derived = crypto.scryptSync(password, salt, record.keyLength, record.params)
|
|
45
|
-
if (expected.length !== derived.length) {
|
|
46
|
-
return false
|
|
47
|
-
}
|
|
48
|
-
return crypto.timingSafeEqual(expected, Buffer.from(derived))
|
|
49
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import crypto from "crypto"
|
|
2
|
-
|
|
3
|
-
export interface SessionInfo {
|
|
4
|
-
id: string
|
|
5
|
-
createdAt: number
|
|
6
|
-
username: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class SessionManager {
|
|
10
|
-
private sessions = new Map<string, SessionInfo>()
|
|
11
|
-
|
|
12
|
-
createSession(username: string): SessionInfo {
|
|
13
|
-
const id = crypto.randomBytes(32).toString("base64url")
|
|
14
|
-
const info: SessionInfo = { id, createdAt: Date.now(), username }
|
|
15
|
-
this.sessions.set(id, info)
|
|
16
|
-
return info
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
getSession(id: string | undefined): SessionInfo | undefined {
|
|
20
|
-
if (!id) return undefined
|
|
21
|
-
return this.sessions.get(id)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import crypto from "crypto"
|
|
2
|
-
|
|
3
|
-
export interface BootstrapToken {
|
|
4
|
-
token: string
|
|
5
|
-
createdAt: number
|
|
6
|
-
consumed: boolean
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class TokenManager {
|
|
10
|
-
private token: BootstrapToken | null = null
|
|
11
|
-
|
|
12
|
-
constructor(private readonly ttlMs: number) {}
|
|
13
|
-
|
|
14
|
-
generate(): string {
|
|
15
|
-
const token = crypto.randomBytes(32).toString("base64url")
|
|
16
|
-
this.token = { token, createdAt: Date.now(), consumed: false }
|
|
17
|
-
return token
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
consume(token: string): boolean {
|
|
21
|
-
if (!this.token) return false
|
|
22
|
-
if (this.token.consumed) return false
|
|
23
|
-
if (Date.now() - this.token.createdAt > this.ttlMs) return false
|
|
24
|
-
if (token !== this.token.token) return false
|
|
25
|
-
this.token.consumed = true
|
|
26
|
-
return true
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
peek(): string | null {
|
|
30
|
-
return this.token?.token ?? null
|
|
31
|
-
}
|
|
32
|
-
}
|