ework-web 0.10.24 → 0.10.26
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/src/auth-hook.ts +81 -0
- package/src/auth.ts +13 -0
- package/src/config.ts +5 -1
- package/src/index.ts +2 -0
- package/src/store.ts +16 -0
package/package.json
CHANGED
package/src/auth-hook.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { provisionUser, type UserRow } from "./store";
|
|
2
|
+
|
|
3
|
+
export interface AuthHookResult {
|
|
4
|
+
/** Must match /^[A-Za-z0-9_-]{1,64}$/ — enforced for cookie-safety. */
|
|
5
|
+
login: string;
|
|
6
|
+
/** If true, user is promoted to site-admin (one-way: never demotes). */
|
|
7
|
+
isAdmin?: boolean;
|
|
8
|
+
/** User kind for auto-provisioning. Defaults to "human". */
|
|
9
|
+
kind?: "human" | "bot" | "system";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AuthProvider {
|
|
13
|
+
/**
|
|
14
|
+
* Return user identity or null. Do NOT throw on auth failure —
|
|
15
|
+
* return null. Exceptions are caught and logged by the caller.
|
|
16
|
+
*/
|
|
17
|
+
authenticate(req: Request): Promise<AuthHookResult | null>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LOGIN_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
21
|
+
const hookCache = new Map<string, AuthProvider | null>();
|
|
22
|
+
|
|
23
|
+
async function loadHook(hookPath: string): Promise<AuthProvider | null> {
|
|
24
|
+
const cached = hookCache.get(hookPath);
|
|
25
|
+
if (cached !== undefined) return cached;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const path = await import("node:path");
|
|
29
|
+
const { pathToFileURL } = await import("node:url");
|
|
30
|
+
const abs = path.isAbsolute(hookPath) ? hookPath : path.resolve(process.cwd(), hookPath);
|
|
31
|
+
const mod = await import(pathToFileURL(abs).href);
|
|
32
|
+
const provider: AuthProvider = mod.default ?? mod;
|
|
33
|
+
if (!provider || typeof provider.authenticate !== "function") {
|
|
34
|
+
console.warn(`[auth-hook] ${hookPath}: must export an object with authenticate()`);
|
|
35
|
+
hookCache.set(hookPath, null);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
hookCache.set(hookPath, provider);
|
|
39
|
+
return provider;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.warn(`[auth-hook] failed to load ${hookPath}:`, e);
|
|
42
|
+
hookCache.set(hookPath, null);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runAuthHook(hookPath: string, req: Request): Promise<UserRow | null> {
|
|
48
|
+
if (!hookPath) return null;
|
|
49
|
+
|
|
50
|
+
const provider = await loadHook(hookPath);
|
|
51
|
+
if (!provider) return null;
|
|
52
|
+
|
|
53
|
+
let result: AuthHookResult | null;
|
|
54
|
+
try {
|
|
55
|
+
result = await provider.authenticate(req);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.warn(`[auth-hook] ${hookPath}: authenticate() threw:`, e);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!result?.login) return null;
|
|
62
|
+
|
|
63
|
+
// v2 cookies split on "." — LOGIN_RE prevents dots/special chars that
|
|
64
|
+
// would break the split or enable cookie injection.
|
|
65
|
+
if (!LOGIN_RE.test(result.login)) {
|
|
66
|
+
console.warn(`[auth-hook] ${hookPath}: invalid login format (must match ${LOGIN_RE})`);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const user = await provisionUser(result.login, {
|
|
71
|
+
kind: result.kind ?? "human",
|
|
72
|
+
isAdmin: result.isAdmin ?? false,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!user.is_active) return null;
|
|
76
|
+
return user;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function clearAuthHookCache(): void {
|
|
80
|
+
hookCache.clear();
|
|
81
|
+
}
|
package/src/auth.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Config } from "./config";
|
|
2
2
|
import { getUserByLogin, ensureUser, verifyPat, type UserRow } from "./store";
|
|
3
|
+
import { runAuthHook } from "./auth-hook";
|
|
3
4
|
|
|
4
5
|
// Per-user token-cookie auth. Cookie value is HMAC-signed and carries login +
|
|
5
6
|
// issued-at, so the server is stateless (no session table). Two cookie formats
|
|
@@ -135,6 +136,18 @@ export async function checkAuth(req: Request, cfg: Config, ip?: string | null):
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
// Internal auth hook (daemon / machine-to-machine — permanent credentials).
|
|
140
|
+
if (cfg.internalAuthHook) {
|
|
141
|
+
const user = await runAuthHook(cfg.internalAuthHook, req);
|
|
142
|
+
if (user) return { ok: true, user };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// User auth hook (human users — may expire, e.g. SSO/OAuth sessions).
|
|
146
|
+
if (cfg.userAuthHook) {
|
|
147
|
+
const user = await runAuthHook(cfg.userAuthHook, req);
|
|
148
|
+
if (user) return { ok: true, user };
|
|
149
|
+
}
|
|
150
|
+
|
|
138
151
|
return { ok: false, user: null };
|
|
139
152
|
}
|
|
140
153
|
|
package/src/config.ts
CHANGED
|
@@ -79,6 +79,8 @@ export const configSchema = z.object({
|
|
|
79
79
|
daemonWebhookUrl: z.string().default(""),
|
|
80
80
|
daemonWebhookSecret: z.string().default(""),
|
|
81
81
|
routerAdminToken: z.string().default(""),
|
|
82
|
+
internalAuthHook: z.string().default(""),
|
|
83
|
+
userAuthHook: z.string().default(""),
|
|
82
84
|
// Default "provider/model" string passed to `opencode run --model <X>`.
|
|
83
85
|
// Empty = let opencode pick per its own opencode.json + env. ework-daemon
|
|
84
86
|
// pushes this (or the per-project override) on every spawn to defend
|
|
@@ -178,7 +180,9 @@ export async function loadConfig(): Promise<Config> {
|
|
|
178
180
|
daemonBotLogin: process.env.WORK_DAEMON_BOT_LOGIN ?? "",
|
|
179
181
|
daemonWebhookUrl: process.env.WORK_DAEMON_WEBHOOK_URL ?? "",
|
|
180
182
|
daemonWebhookSecret: process.env.WORK_DAEMON_WEBHOOK_SECRET ?? "",
|
|
181
|
-
|
|
183
|
+
routerAdminToken: process.env.WORK_ROUTER_ADMIN_TOKEN ?? "",
|
|
184
|
+
internalAuthHook: process.env.WORK_INTERNAL_AUTH_HOOK ?? "",
|
|
185
|
+
userAuthHook: process.env.WORK_USER_AUTH_HOOK ?? "",
|
|
182
186
|
defaultModel: db.defaultModel ?? process.env.WORK_DEFAULT_MODEL,
|
|
183
187
|
autowireActive: process.env.WORK_AUTOWIRE_ACTIVE !== "false",
|
|
184
188
|
webhookMaxConcurrent: Number(process.env.WORK_WEBHOOK_MAX_CONCURRENT ?? "6"),
|
package/src/index.ts
CHANGED
|
@@ -454,6 +454,8 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
454
454
|
if (url.pathname === "/static/app.js") return staticAsset("app.js", "text/javascript; charset=utf-8", req);
|
|
455
455
|
if (url.pathname === "/static/db-wizard.js") return staticAsset("db-wizard.js", "text/javascript; charset=utf-8", req);
|
|
456
456
|
if (url.pathname === "/static/daemon-mgr.js") return staticAsset("daemon-mgr.js", "text/javascript; charset=utf-8", req);
|
|
457
|
+
if (url.pathname === "/static/daemon-groups.js") return staticAsset("daemon-groups.js", "text/javascript; charset=utf-8", req);
|
|
458
|
+
if (url.pathname === "/static/label-picker.js") return staticAsset("label-picker.js", "text/javascript; charset=utf-8", req);
|
|
457
459
|
if (url.pathname === "/static/session.js") return staticAsset("session.js", "text/javascript; charset=utf-8", req);
|
|
458
460
|
if (url.pathname === "/static/file.js") return staticAsset("file.js", "text/javascript; charset=utf-8", req);
|
|
459
461
|
if (url.pathname === "/static/tts.js") return staticAsset("tts.js", "text/javascript; charset=utf-8", req);
|
package/src/store.ts
CHANGED
|
@@ -138,6 +138,22 @@ export async function ensureUser(login: string, kind: UserKind = "human"): Promi
|
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
export async function provisionUser(
|
|
142
|
+
login: string,
|
|
143
|
+
opts: { kind?: UserKind; isAdmin?: boolean },
|
|
144
|
+
): Promise<UserRow> {
|
|
145
|
+
const user = await ensureUser(login, opts.kind ?? "human");
|
|
146
|
+
if (opts.isAdmin && !user.is_admin) {
|
|
147
|
+
const db = getDB();
|
|
148
|
+
await db.run("UPDATE {{users}} SET is_admin = 1, updated_at = ? WHERE login = ?", [
|
|
149
|
+
now(),
|
|
150
|
+
login,
|
|
151
|
+
]);
|
|
152
|
+
return { ...user, is_admin: 1 };
|
|
153
|
+
}
|
|
154
|
+
return user;
|
|
155
|
+
}
|
|
156
|
+
|
|
141
157
|
export async function getProject(owner: string, name: string): Promise<ProjectRow | null> {
|
|
142
158
|
return (await getDB().get<ProjectRow>("SELECT * FROM {{projects}} WHERE owner = ? AND name = ?", [owner, name])) ?? null;
|
|
143
159
|
}
|