@cjhyy/code-shell-server 0.9.7 → 0.9.9
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 +85 -23
- package/dist/bin/code-shell-project-runtime.d.ts +2 -0
- package/dist/bin/code-shell-project-runtime.js +8 -0
- package/dist/desktop-web/http-api.d.ts +43 -0
- package/dist/desktop-web/http-api.js +376 -0
- package/dist/hub/approval-lease.d.ts +30 -0
- package/dist/hub/approval-lease.js +89 -0
- package/dist/hub/auth-http.d.ts +23 -0
- package/dist/hub/auth-http.js +259 -0
- package/dist/hub/auth-store.d.ts +52 -0
- package/dist/hub/auth-store.js +316 -0
- package/dist/hub/configuration.d.ts +64 -0
- package/dist/hub/configuration.js +555 -0
- package/dist/hub/files.d.ts +9 -0
- package/dist/hub/files.js +278 -0
- package/dist/hub/mcp-configuration.d.ts +51 -0
- package/dist/hub/mcp-configuration.js +558 -0
- package/dist/hub/mcp-probe.d.ts +24 -0
- package/dist/hub/mcp-probe.js +392 -0
- package/dist/hub/model-probe.d.ts +20 -0
- package/dist/hub/model-probe.js +193 -0
- package/dist/hub/outbound-transport.d.ts +36 -0
- package/dist/hub/outbound-transport.js +122 -0
- package/dist/hub/run-replay.d.ts +42 -0
- package/dist/hub/run-replay.js +89 -0
- package/dist/hub/session-management.d.ts +36 -0
- package/dist/hub/session-management.js +405 -0
- package/dist/hub/skills-management.d.ts +43 -0
- package/dist/hub/skills-management.js +487 -0
- package/dist/hub/uploads.d.ts +22 -0
- package/dist/hub/uploads.js +241 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.desktop-web.d.ts +2 -0
- package/dist/index.desktop-web.js +2 -0
- package/dist/index.js +3 -0
- package/dist/index.links.d.ts +6 -0
- package/dist/index.links.js +6 -0
- package/dist/index.panels.d.ts +9 -0
- package/dist/index.panels.js +8 -0
- package/dist/index.serve.d.ts +1 -0
- package/dist/index.serve.js +1 -0
- package/dist/index.storage.d.ts +2 -0
- package/dist/index.storage.js +2 -0
- package/dist/links/catalog.d.ts +10 -0
- package/dist/links/catalog.js +41 -0
- package/dist/links/device-oauth.d.ts +52 -0
- package/dist/links/device-oauth.js +255 -0
- package/dist/links/http.d.ts +34 -0
- package/dist/links/http.js +144 -0
- package/dist/links/service.d.ts +54 -0
- package/dist/links/service.js +578 -0
- package/dist/links/types.d.ts +6 -0
- package/dist/links/types.js +1 -0
- package/dist/mobile-remote/mobile-client-event-validator.js +5 -2
- package/dist/mobile-remote/pending-approvals.js +2 -0
- package/dist/mobile-remote/remote-host-manager.d.ts +8 -0
- package/dist/mobile-remote/remote-host-manager.js +90 -15
- package/dist/mobile-remote/room-manager.js +17 -5
- package/dist/panels/agent-task-host.d.ts +32 -0
- package/dist/panels/agent-task-host.js +457 -0
- package/dist/panels/agent-task-models.d.ts +27 -0
- package/dist/panels/agent-task-models.js +51 -0
- package/dist/panels/agent-task-service.d.ts +105 -0
- package/dist/panels/agent-task-service.js +283 -0
- package/dist/panels/http.d.ts +34 -0
- package/dist/panels/http.js +37 -0
- package/dist/panels/hub-binding.d.ts +12 -0
- package/dist/panels/hub-binding.js +174 -0
- package/dist/panels/management-http.d.ts +29 -0
- package/dist/panels/management-http.js +119 -0
- package/dist/panels/management.d.ts +39 -0
- package/dist/panels/management.js +494 -0
- package/dist/panels/process-files.d.ts +14 -0
- package/dist/panels/process-files.js +299 -0
- package/dist/panels/process-service.d.ts +96 -0
- package/dist/panels/process-service.js +501 -0
- package/dist/panels/runtime-services.d.ts +24 -0
- package/dist/panels/runtime-services.js +462 -0
- package/dist/panels/runtime.d.ts +51 -0
- package/dist/panels/runtime.js +1063 -0
- package/dist/panels/storage-store.d.ts +9 -0
- package/dist/panels/storage-store.js +154 -0
- package/dist/panels/types.d.ts +52 -0
- package/dist/panels/types.js +1 -0
- package/dist/project-runtime/control-server.d.ts +19 -0
- package/dist/project-runtime/control-server.js +300 -0
- package/dist/project-runtime/docker-provider.d.ts +33 -0
- package/dist/project-runtime/docker-provider.js +487 -0
- package/dist/project-runtime/lock.d.ts +6 -0
- package/dist/project-runtime/lock.js +43 -0
- package/dist/project-runtime/managed-entry.d.ts +10 -0
- package/dist/project-runtime/managed-entry.js +113 -0
- package/dist/project-runtime/manager.d.ts +40 -0
- package/dist/project-runtime/manager.js +249 -0
- package/dist/project-runtime/proxy.d.ts +22 -0
- package/dist/project-runtime/proxy.js +706 -0
- package/dist/project-runtime/registry.d.ts +54 -0
- package/dist/project-runtime/registry.js +286 -0
- package/dist/project-runtime/types.d.ts +43 -0
- package/dist/project-runtime/types.js +1 -0
- package/dist/serve/cli.d.ts +5 -0
- package/dist/serve/cli.js +82 -23
- package/dist/serve/headless-server.d.ts +11 -0
- package/dist/serve/headless-server.js +737 -83
- package/dist/session-titles-store.d.ts +17 -0
- package/dist/session-titles-store.js +174 -0
- package/dist/static-files.js +1 -0
- package/dist/worker-bridge-core.js +17 -6
- package/package.json +19 -5
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export class ApprovalLeases {
|
|
2
|
+
onChange;
|
|
3
|
+
ttlMs;
|
|
4
|
+
now;
|
|
5
|
+
entries = new Map();
|
|
6
|
+
constructor(onChange, ttlMs = 30_000, now = Date.now) {
|
|
7
|
+
this.onChange = onChange;
|
|
8
|
+
this.ttlMs = ttlMs;
|
|
9
|
+
this.now = now;
|
|
10
|
+
}
|
|
11
|
+
add(approval) {
|
|
12
|
+
if (this.entries.has(approval.requestId))
|
|
13
|
+
return;
|
|
14
|
+
if (this.entries.size >= 256)
|
|
15
|
+
throw new Error("too many pending approvals");
|
|
16
|
+
this.entries.set(approval.requestId, {
|
|
17
|
+
approval,
|
|
18
|
+
holderId: null,
|
|
19
|
+
expiresAt: 0,
|
|
20
|
+
submitted: false,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
get(requestId) {
|
|
24
|
+
return this.entries.get(requestId)?.approval;
|
|
25
|
+
}
|
|
26
|
+
snapshot() {
|
|
27
|
+
return [...this.entries.values()]
|
|
28
|
+
.filter((entry) => !entry.submitted)
|
|
29
|
+
.map((entry) => entry.approval);
|
|
30
|
+
}
|
|
31
|
+
claim(requestId, holderId) {
|
|
32
|
+
this.sweep();
|
|
33
|
+
const entry = this.entries.get(requestId);
|
|
34
|
+
if (!entry || entry.submitted)
|
|
35
|
+
throw new Error("approval is no longer pending");
|
|
36
|
+
if (entry.holderId && entry.holderId !== holderId) {
|
|
37
|
+
throw new Error("approval is being handled on another device");
|
|
38
|
+
}
|
|
39
|
+
entry.holderId = holderId;
|
|
40
|
+
entry.expiresAt = this.now() + this.ttlMs;
|
|
41
|
+
const lease = { requestId, holderId, expiresAt: entry.expiresAt };
|
|
42
|
+
this.onChange(lease);
|
|
43
|
+
return lease;
|
|
44
|
+
}
|
|
45
|
+
/** Hold until worker acknowledgement; expiry must never permit duplicate decisions. */
|
|
46
|
+
submit(requestId, holderId) {
|
|
47
|
+
this.claim(requestId, holderId);
|
|
48
|
+
const entry = this.entries.get(requestId);
|
|
49
|
+
entry.submitted = true;
|
|
50
|
+
return entry.approval;
|
|
51
|
+
}
|
|
52
|
+
release(requestId, holderId) {
|
|
53
|
+
const entry = this.entries.get(requestId);
|
|
54
|
+
if (!entry || entry.submitted || entry.holderId !== holderId)
|
|
55
|
+
return;
|
|
56
|
+
entry.holderId = null;
|
|
57
|
+
entry.expiresAt = 0;
|
|
58
|
+
this.onChange({ requestId, holderId: null, expiresAt: null });
|
|
59
|
+
}
|
|
60
|
+
releaseHolder(holderId) {
|
|
61
|
+
for (const requestId of this.entries.keys())
|
|
62
|
+
this.release(requestId, holderId);
|
|
63
|
+
}
|
|
64
|
+
resolved(requestId) {
|
|
65
|
+
if (this.entries.delete(requestId)) {
|
|
66
|
+
this.onChange({ requestId, holderId: null, expiresAt: null });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
retry(requestId) {
|
|
70
|
+
const entry = this.entries.get(requestId);
|
|
71
|
+
if (!entry)
|
|
72
|
+
return;
|
|
73
|
+
entry.submitted = false;
|
|
74
|
+
entry.holderId = null;
|
|
75
|
+
entry.expiresAt = 0;
|
|
76
|
+
this.onChange({ requestId, holderId: null, expiresAt: null });
|
|
77
|
+
}
|
|
78
|
+
clear() {
|
|
79
|
+
for (const id of this.entries.keys())
|
|
80
|
+
this.resolved(id);
|
|
81
|
+
}
|
|
82
|
+
sweep() {
|
|
83
|
+
for (const [requestId, entry] of this.entries) {
|
|
84
|
+
if (!entry.submitted && entry.holderId && entry.expiresAt <= this.now()) {
|
|
85
|
+
this.release(requestId, entry.holderId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { HubAuthStore, type HubSession } from "./auth-store.js";
|
|
3
|
+
export declare const HUB_SESSION_COOKIE = "cs_hub_session";
|
|
4
|
+
export interface HubAuthOptions {
|
|
5
|
+
dataDir: string;
|
|
6
|
+
/** Trusted external URL, also used behind a TLS-terminating reverse proxy. */
|
|
7
|
+
publicOrigin?: string;
|
|
8
|
+
onRevoke?: (sessionId: string) => void;
|
|
9
|
+
now?: () => number;
|
|
10
|
+
sessionTtlMs?: number;
|
|
11
|
+
maxLoginAttempts?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface HubAuth {
|
|
14
|
+
bootstrapToken?: string;
|
|
15
|
+
store: HubAuthStore;
|
|
16
|
+
handle(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
17
|
+
authenticate(req: IncomingMessage): Promise<HubSession | null>;
|
|
18
|
+
isOriginAllowed(req: IncomingMessage): boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Shared single-user HTTP authentication; no Electron or worker dependency. */
|
|
21
|
+
export declare function createHubAuth(options: HubAuthOptions): Promise<HubAuth>;
|
|
22
|
+
/** Authorization wins over cookies; malformed bearer headers fail closed. */
|
|
23
|
+
export declare function readHubSessionToken(req: Pick<IncomingMessage, "headers">): string | undefined;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { HubAuthError, HubAuthStore, } from "./auth-store.js";
|
|
2
|
+
export const HUB_SESSION_COOKIE = "cs_hub_session";
|
|
3
|
+
const MAX_BODY_BYTES = 8 * 1024;
|
|
4
|
+
const LOGIN_WINDOW_MS = 60_000;
|
|
5
|
+
const MAX_ATTEMPT_SOURCES = 1_024;
|
|
6
|
+
/** Shared single-user HTTP authentication; no Electron or worker dependency. */
|
|
7
|
+
export async function createHubAuth(options) {
|
|
8
|
+
const now = options.now ?? Date.now;
|
|
9
|
+
const origin = options.publicOrigin ? normalizeOrigin(options.publicOrigin) : undefined;
|
|
10
|
+
const secureCookie = origin?.startsWith("https:") ?? false;
|
|
11
|
+
const maxLoginAttempts = options.maxLoginAttempts ?? 10;
|
|
12
|
+
if (!Number.isSafeInteger(maxLoginAttempts) || maxLoginAttempts < 1) {
|
|
13
|
+
throw new Error("Hub login attempt limit must be positive");
|
|
14
|
+
}
|
|
15
|
+
const store = new HubAuthStore(options);
|
|
16
|
+
const bootstrapToken = store.initialize();
|
|
17
|
+
const attempts = new Map();
|
|
18
|
+
let activeLogins = 0;
|
|
19
|
+
let globalAttempts = { count: 0, resetAt: now() + LOGIN_WINDOW_MS };
|
|
20
|
+
function isOriginAllowed(req) {
|
|
21
|
+
const requestOrigin = req.headers.origin;
|
|
22
|
+
const fetchSite = req.headers["sec-fetch-site"];
|
|
23
|
+
if (fetchSite === "cross-site")
|
|
24
|
+
return false;
|
|
25
|
+
const expected = origin ?? requestOriginFromHost(req);
|
|
26
|
+
if (expected === undefined)
|
|
27
|
+
return false;
|
|
28
|
+
// Address-bar navigation and noreferrer links omit Origin and report "none".
|
|
29
|
+
// Permit only read-only top-level navigation; writes, fetches and socket
|
|
30
|
+
// upgrades still use the existing same-origin/native-client checks.
|
|
31
|
+
if (requestOrigin === undefined)
|
|
32
|
+
return (fetchSite === undefined ||
|
|
33
|
+
fetchSite === "same-origin" ||
|
|
34
|
+
(fetchSite === "none" &&
|
|
35
|
+
(req.method === "GET" || req.method === "HEAD") &&
|
|
36
|
+
req.headers["sec-fetch-mode"] === "navigate" &&
|
|
37
|
+
req.headers["sec-fetch-dest"] === "document"));
|
|
38
|
+
if (typeof requestOrigin !== "string" || requestOrigin === "null")
|
|
39
|
+
return false;
|
|
40
|
+
return requestOrigin === expected;
|
|
41
|
+
}
|
|
42
|
+
function takeAttempt(req) {
|
|
43
|
+
const time = now();
|
|
44
|
+
for (const [key, bucket] of attempts) {
|
|
45
|
+
if (bucket.resetAt <= time)
|
|
46
|
+
attempts.delete(key);
|
|
47
|
+
}
|
|
48
|
+
if (globalAttempts.resetAt <= time)
|
|
49
|
+
globalAttempts = { count: 0, resetAt: time + LOGIN_WINDOW_MS };
|
|
50
|
+
// Proxy forwarding headers are intentionally not trusted for rate limits.
|
|
51
|
+
const key = req.socket.remoteAddress ?? "unknown";
|
|
52
|
+
let bucket = attempts.get(key);
|
|
53
|
+
if (!bucket) {
|
|
54
|
+
if (attempts.size >= MAX_ATTEMPT_SOURCES)
|
|
55
|
+
throw new HubAuthError(429, "Too many login attempts; retry in one minute");
|
|
56
|
+
bucket = { count: 0, resetAt: time + LOGIN_WINDOW_MS };
|
|
57
|
+
attempts.set(key, bucket);
|
|
58
|
+
}
|
|
59
|
+
if (bucket.count >= maxLoginAttempts || globalAttempts.count >= 100 || activeLogins >= 4) {
|
|
60
|
+
throw new HubAuthError(429, "Too many login attempts; retry in one minute");
|
|
61
|
+
}
|
|
62
|
+
bucket.count += 1;
|
|
63
|
+
globalAttempts.count += 1;
|
|
64
|
+
return key;
|
|
65
|
+
}
|
|
66
|
+
function setSessionCookie(res, token, maxAge) {
|
|
67
|
+
res.setHeader("Set-Cookie", `${HUB_SESSION_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${maxAge}${secureCookie ? "; Secure" : ""}`);
|
|
68
|
+
}
|
|
69
|
+
function completeLogin(res, grant) {
|
|
70
|
+
for (const sessionId of grant.revokedSessionIds)
|
|
71
|
+
options.onRevoke?.(sessionId);
|
|
72
|
+
setSessionCookie(res, grant.token, Math.floor(store.sessionTtlMs / 1_000));
|
|
73
|
+
json(res, 200, { authenticated: true, session: grant.session });
|
|
74
|
+
}
|
|
75
|
+
async function authenticate(req) {
|
|
76
|
+
return store.authenticate(readHubSessionToken(req));
|
|
77
|
+
}
|
|
78
|
+
async function handle(req, res) {
|
|
79
|
+
const path = (req.url ?? "/").split("?", 1)[0];
|
|
80
|
+
if (path !== "/api/v1/auth" && !path.startsWith("/api/v1/auth/"))
|
|
81
|
+
return false;
|
|
82
|
+
res.setHeader("Cache-Control", "no-store");
|
|
83
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
84
|
+
try {
|
|
85
|
+
const method = req.method ?? "GET";
|
|
86
|
+
if (method !== "GET" && method !== "HEAD" && !isOriginAllowed(req)) {
|
|
87
|
+
throw new HubAuthError(403, "Request origin is not allowed");
|
|
88
|
+
}
|
|
89
|
+
if (path === "/api/v1/auth/status" && method === "GET") {
|
|
90
|
+
const session = await authenticate(req);
|
|
91
|
+
json(res, 200, {
|
|
92
|
+
initialized: store.isInitialized(),
|
|
93
|
+
authenticated: !!session,
|
|
94
|
+
...(session ? { session } : {}),
|
|
95
|
+
});
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if ((path === "/api/v1/auth/setup" || path === "/api/v1/auth/login") && method === "POST") {
|
|
99
|
+
const key = takeAttempt(req);
|
|
100
|
+
activeLogins += 1;
|
|
101
|
+
try {
|
|
102
|
+
const body = await readJsonBody(req);
|
|
103
|
+
const input = body;
|
|
104
|
+
const grant = path.endsWith("/setup")
|
|
105
|
+
? await store.setup(input)
|
|
106
|
+
: await store.login(input, readHubSessionToken(req));
|
|
107
|
+
attempts.delete(key);
|
|
108
|
+
completeLogin(res, grant);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
activeLogins -= 1;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const session = await authenticate(req);
|
|
116
|
+
if (!session)
|
|
117
|
+
throw new HubAuthError(401, "Authentication required");
|
|
118
|
+
if (path === "/api/v1/auth/sessions" && method === "GET") {
|
|
119
|
+
json(res, 200, {
|
|
120
|
+
sessions: store.listSessions().map((row) => ({ ...row, current: row.id === session.id })),
|
|
121
|
+
});
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (path === "/api/v1/auth/logout" && method === "POST") {
|
|
125
|
+
await readJsonBody(req);
|
|
126
|
+
store.revoke(session.id);
|
|
127
|
+
options.onRevoke?.(session.id);
|
|
128
|
+
setSessionCookie(res, "", 0);
|
|
129
|
+
json(res, 200, { ok: true });
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
const revokeMatch = /^\/api\/v1\/auth\/sessions\/([a-f0-9-]{36})$/.exec(path);
|
|
133
|
+
if (revokeMatch && method === "DELETE") {
|
|
134
|
+
if (!store.revoke(revokeMatch[1]))
|
|
135
|
+
throw new HubAuthError(404, "Session not found");
|
|
136
|
+
options.onRevoke?.(revokeMatch[1]);
|
|
137
|
+
if (revokeMatch[1] === session.id)
|
|
138
|
+
setSessionCookie(res, "", 0);
|
|
139
|
+
json(res, 200, { ok: true });
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
throw new HubAuthError(404, "Auth endpoint not found");
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
const status = error instanceof HubAuthError ? error.statusCode : 503;
|
|
146
|
+
if (status === 429)
|
|
147
|
+
res.setHeader("Retry-After", "60");
|
|
148
|
+
json(res, status, {
|
|
149
|
+
error: error instanceof HubAuthError ? error.message : "Authentication service unavailable",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return { bootstrapToken, store, handle, authenticate, isOriginAllowed };
|
|
155
|
+
}
|
|
156
|
+
/** Authorization wins over cookies; malformed bearer headers fail closed. */
|
|
157
|
+
export function readHubSessionToken(req) {
|
|
158
|
+
if (req.headers.authorization !== undefined) {
|
|
159
|
+
const match = /^Bearer ([A-Za-z0-9_-]{43})$/i.exec(req.headers.authorization);
|
|
160
|
+
return match?.[1];
|
|
161
|
+
}
|
|
162
|
+
const entries = req.headers.cookie
|
|
163
|
+
?.split(";")
|
|
164
|
+
.map((entry) => entry.trim())
|
|
165
|
+
.filter((entry) => entry.startsWith(`${HUB_SESSION_COOKIE}=`));
|
|
166
|
+
if (!entries || entries.length !== 1)
|
|
167
|
+
return undefined;
|
|
168
|
+
const token = entries[0].slice(HUB_SESSION_COOKIE.length + 1);
|
|
169
|
+
return /^[A-Za-z0-9_-]{43}$/.test(token) ? token : undefined;
|
|
170
|
+
}
|
|
171
|
+
function normalizeOrigin(input) {
|
|
172
|
+
const url = new URL(input);
|
|
173
|
+
if (!["http:", "https:"].includes(url.protocol) ||
|
|
174
|
+
(url.protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) ||
|
|
175
|
+
url.username ||
|
|
176
|
+
url.password ||
|
|
177
|
+
url.pathname !== "/" ||
|
|
178
|
+
url.search ||
|
|
179
|
+
url.hash) {
|
|
180
|
+
throw new Error("Hub public origin must use HTTPS (HTTP allowed only on loopback) without credentials, path, query, or fragment");
|
|
181
|
+
}
|
|
182
|
+
return url.origin;
|
|
183
|
+
}
|
|
184
|
+
function requestOriginFromHost(req) {
|
|
185
|
+
const host = req.headers.host;
|
|
186
|
+
if (!host || /[\s/\\@?#]/.test(host))
|
|
187
|
+
return undefined;
|
|
188
|
+
const protocol = req.socket.encrypted ? "https" : "http";
|
|
189
|
+
try {
|
|
190
|
+
return normalizeOrigin(`${protocol}://${host}`);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function json(res, statusCode, body) {
|
|
197
|
+
if (res.destroyed || res.writableEnded)
|
|
198
|
+
return;
|
|
199
|
+
res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
|
|
200
|
+
res.end(JSON.stringify(body));
|
|
201
|
+
}
|
|
202
|
+
function readJsonBody(req) {
|
|
203
|
+
const contentType = req.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
|
|
204
|
+
if (contentType !== "application/json")
|
|
205
|
+
throw new HubAuthError(415, "Content-Type must be application/json");
|
|
206
|
+
if (Number(req.headers["content-length"] ?? 0) > MAX_BODY_BYTES)
|
|
207
|
+
throw new HubAuthError(413, "Auth request is too large");
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const chunks = [];
|
|
210
|
+
let bytes = 0;
|
|
211
|
+
const timer = setTimeout(() => fail(new HubAuthError(408, "Auth request timed out")), 10_000);
|
|
212
|
+
timer.unref();
|
|
213
|
+
function cleanup() {
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
req.off("data", onData);
|
|
216
|
+
req.off("end", onEnd);
|
|
217
|
+
req.off("aborted", onAborted);
|
|
218
|
+
req.off("error", onError);
|
|
219
|
+
}
|
|
220
|
+
function fail(error) {
|
|
221
|
+
cleanup();
|
|
222
|
+
// Aborting a Node request can emit error after aborted. Keep a listener
|
|
223
|
+
// after detaching body readers so a disconnected client cannot crash the host.
|
|
224
|
+
req.once("error", () => { });
|
|
225
|
+
req.resume();
|
|
226
|
+
reject(error);
|
|
227
|
+
}
|
|
228
|
+
function onData(chunk) {
|
|
229
|
+
bytes += chunk.length;
|
|
230
|
+
if (bytes > MAX_BODY_BYTES) {
|
|
231
|
+
fail(new HubAuthError(413, "Auth request is too large"));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
chunks.push(chunk);
|
|
235
|
+
}
|
|
236
|
+
function onEnd() {
|
|
237
|
+
cleanup();
|
|
238
|
+
try {
|
|
239
|
+
const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
240
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
241
|
+
throw new Error("not an object");
|
|
242
|
+
resolve(value);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
reject(new HubAuthError(400, "Request body must be a JSON object"));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function onAborted() {
|
|
249
|
+
fail(new HubAuthError(400, "Auth request aborted"));
|
|
250
|
+
}
|
|
251
|
+
function onError() {
|
|
252
|
+
fail(new HubAuthError(400, "Unable to read auth request"));
|
|
253
|
+
}
|
|
254
|
+
req.on("data", onData);
|
|
255
|
+
req.once("end", onEnd);
|
|
256
|
+
req.once("aborted", onAborted);
|
|
257
|
+
req.once("error", onError);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export declare const HUB_SESSION_TTL_MS: number;
|
|
2
|
+
export interface HubSession {
|
|
3
|
+
id: string;
|
|
4
|
+
username: string;
|
|
5
|
+
deviceName: string;
|
|
6
|
+
createdAt: number;
|
|
7
|
+
lastSeenAt: number;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
}
|
|
10
|
+
export interface HubLoginInput {
|
|
11
|
+
username: string;
|
|
12
|
+
password: string;
|
|
13
|
+
deviceName?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface HubSessionGrant {
|
|
16
|
+
token: string;
|
|
17
|
+
session: HubSession;
|
|
18
|
+
revokedSessionIds: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare class HubAuthError extends Error {
|
|
21
|
+
readonly statusCode: number;
|
|
22
|
+
constructor(statusCode: number, message: string);
|
|
23
|
+
}
|
|
24
|
+
export declare class HubAuthStore {
|
|
25
|
+
readonly filePath: string;
|
|
26
|
+
readonly sessionTtlMs: number;
|
|
27
|
+
private readonly now;
|
|
28
|
+
private readonly maxSessions;
|
|
29
|
+
constructor(options: {
|
|
30
|
+
dataDir: string;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
sessionTtlMs?: number;
|
|
33
|
+
maxSessions?: number;
|
|
34
|
+
});
|
|
35
|
+
/** Only the process that creates the store receives the one-time secret. */
|
|
36
|
+
initialize(): string | undefined;
|
|
37
|
+
isInitialized(): boolean;
|
|
38
|
+
setup(input: HubLoginInput & {
|
|
39
|
+
token: string;
|
|
40
|
+
}): Promise<HubSessionGrant>;
|
|
41
|
+
login(input: HubLoginInput, replaceToken?: string): Promise<HubSessionGrant>;
|
|
42
|
+
authenticate(token: string | undefined): HubSession | null;
|
|
43
|
+
listSessions(): HubSession[];
|
|
44
|
+
revoke(id: string): boolean;
|
|
45
|
+
private checkBootstrap;
|
|
46
|
+
private grant;
|
|
47
|
+
private toPublic;
|
|
48
|
+
private mutate;
|
|
49
|
+
private ensureDirectory;
|
|
50
|
+
private read;
|
|
51
|
+
private write;
|
|
52
|
+
}
|