weifuwu 0.22.2 → 0.23.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/dist/index.js CHANGED
@@ -186,7 +186,14 @@ function serve(handler, options) {
186
186
  if (shuttingDown) return;
187
187
  shuttingDown = true;
188
188
  server.close();
189
- process.exit(0);
189
+ const timer = setTimeout(() => {
190
+ server.closeAllConnections();
191
+ process.exit(0);
192
+ }, 1e4);
193
+ server.on("close", () => {
194
+ clearTimeout(timer);
195
+ process.exit(0);
196
+ });
190
197
  };
191
198
  shutdownHandler = shutdown;
192
199
  process.on("SIGTERM", shutdown);
@@ -232,7 +239,7 @@ function serve(handler, options) {
232
239
  console.log(`weifuwu listening on http://${displayHost}:${_cachedPort}`);
233
240
  });
234
241
  return {
235
- stop: () => {
242
+ stop: (timeoutMs = 1e4) => {
236
243
  if (shutdownHandler) {
237
244
  process.off("SIGTERM", shutdownHandler);
238
245
  process.off("SIGINT", shutdownHandler);
@@ -243,9 +250,16 @@ function serve(handler, options) {
243
250
  resolve14();
244
251
  return;
245
252
  }
253
+ server.close();
246
254
  server.closeIdleConnections();
247
- server.closeAllConnections();
248
- server.close(() => resolve14());
255
+ const timer = setTimeout(() => {
256
+ server.closeAllConnections();
257
+ resolve14();
258
+ }, timeoutMs);
259
+ server.on("close", () => {
260
+ clearTimeout(timer);
261
+ resolve14();
262
+ });
249
263
  });
250
264
  },
251
265
  ready,
@@ -837,7 +851,7 @@ function sendHttpResponseOnSocket(socket, response) {
837
851
 
838
852
  // tsx-context.ts
839
853
  import { useSyncExternalStore, createContext } from "react";
840
- var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, loaderData: {}, env: {}, user: {} };
854
+ var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, loaderData: {}, env: {}, user: {} };
841
855
  var KEY = "__WEIFUWU_CTX_STORE";
842
856
  function getStore() {
843
857
  if (typeof globalThis !== "undefined" && globalThis[KEY]) {
@@ -845,8 +859,9 @@ function getStore() {
845
859
  }
846
860
  const s = {
847
861
  _ctx: DEFAULT_CTX,
848
- _snapshot: { params: DEFAULT_CTX.params, query: DEFAULT_CTX.query, user: DEFAULT_CTX.user, parsed: DEFAULT_CTX.parsed, prefs: DEFAULT_CTX.prefs, env: DEFAULT_CTX.env },
862
+ _snapshot: { params: DEFAULT_CTX.params, query: DEFAULT_CTX.query, user: DEFAULT_CTX.user, parsed: DEFAULT_CTX.parsed, theme: DEFAULT_CTX.theme, i18n: DEFAULT_CTX.i18n, loaderData: DEFAULT_CTX.loaderData, env: DEFAULT_CTX.env },
849
863
  _listeners: /* @__PURE__ */ new Set(),
864
+ _rebuilders: [],
850
865
  _alsGetStore: null
851
866
  };
852
867
  if (typeof globalThis !== "undefined") {
@@ -859,8 +874,18 @@ function __registerAls(getStore2) {
859
874
  store._alsGetStore = getStore2;
860
875
  }
861
876
  function setCtx(value) {
877
+ if (typeof window !== "undefined") {
878
+ for (const r of store._rebuilders) {
879
+ const rebuilt = r(value);
880
+ if (rebuilt) Object.assign(value, rebuilt);
881
+ }
882
+ }
862
883
  store._ctx = { ...store._ctx, ...value };
863
- store._snapshot = { params: store._ctx.params, query: store._ctx.query, user: store._ctx.user, parsed: store._ctx.parsed, prefs: store._ctx.prefs, env: store._ctx.env };
884
+ store._snapshot = { params: store._ctx.params, query: store._ctx.query, user: store._ctx.user, parsed: store._ctx.parsed, theme: store._ctx.theme, i18n: store._ctx.i18n, loaderData: store._ctx.loaderData, env: store._ctx.env };
885
+ if (typeof window !== "undefined") {
886
+ ;
887
+ window.__WEIFUWU_CTX = { ...window.__WEIFUWU_CTX, ...value };
888
+ }
864
889
  store._listeners.forEach((fn) => fn());
865
890
  }
866
891
  var TsxContext = createContext(DEFAULT_CTX);
@@ -965,10 +990,30 @@ function cors(options) {
965
990
 
966
991
  // auth.ts
967
992
  function auth(options) {
968
- if (!options.token && !options.verify && !options.proxy) {
969
- throw new Error("auth() requires at least one of: token, verify, or proxy");
993
+ if (!options.token && !options.verify && !options.proxy && !options.session) {
994
+ throw new Error("auth() requires at least one of: token, verify, proxy, or session");
970
995
  }
971
996
  return async (req, ctx, next) => {
997
+ if (options.session) {
998
+ const sessionUserId = ctx.session?.userId;
999
+ if (sessionUserId !== void 0 && sessionUserId !== null) {
1000
+ if (options.resolveUser) {
1001
+ const userData = await options.resolveUser(sessionUserId);
1002
+ if (userData) {
1003
+ ctx.user = userData;
1004
+ return next(req, ctx);
1005
+ }
1006
+ if (typeof ctx.session?.destroy === "function") {
1007
+ ;
1008
+ ctx.session.destroy();
1009
+ }
1010
+ console.warn(`[${currentTraceId()}] auth: session userId ${sessionUserId} resolved to null`);
1011
+ } else {
1012
+ ctx.user = { id: sessionUserId };
1013
+ return next(req, ctx);
1014
+ }
1015
+ }
1016
+ }
972
1017
  const headerName = options.header ?? "Authorization";
973
1018
  let from = "header";
974
1019
  let header = req.headers.get(headerName);
@@ -2093,18 +2138,21 @@ async function getStreamObject() {
2093
2138
  if (!_ai.streamObject) _ai.streamObject = (await import("ai")).streamObject;
2094
2139
  return _ai.streamObject;
2095
2140
  }
2096
- async function aiStream(handler) {
2141
+ async function aiStream(handler, provider) {
2097
2142
  const r = new Router();
2098
2143
  r.post("/", async (req, ctx) => {
2099
2144
  const options = await handler(req, ctx);
2145
+ if (provider && !options.model) {
2146
+ options.model = provider.model();
2147
+ }
2100
2148
  if (options.schema) {
2101
2149
  const streamObject2 = await getStreamObject();
2102
2150
  const { schema, ...params } = options;
2103
2151
  const result2 = streamObject2({ ...params, schema, output: "object" });
2104
2152
  return result2.toTextStreamResponse();
2105
2153
  }
2106
- const streamText4 = await getStreamText();
2107
- const result = streamText4(options);
2154
+ const streamText3 = await getStreamText();
2155
+ const result = streamText3(options);
2108
2156
  return result.toTextStreamResponse();
2109
2157
  });
2110
2158
  return r;
@@ -2316,22 +2364,21 @@ function runWorkflow(opts = {}) {
2316
2364
  let nodes;
2317
2365
  if (input.nodes && input.nodes.length > 0) {
2318
2366
  nodes = input.nodes;
2319
- } else if (opts.model) {
2367
+ } else {
2368
+ if (!opts.provider && !opts.model) throw new Error('Provide either "nodes", a "model", or a "provider" with a model to generate the workflow from "goal"');
2320
2369
  const toolsDesc = Object.entries(opts.tools ?? {}).map(([k, t]) => `- ${k}: ${t.description}`).join("\n");
2321
- const result = await generateText({
2322
- model: opts.model,
2323
- system: [
2324
- "You are a workflow generator. Given a user goal and available tools, output a workflow JSON.",
2325
- "",
2326
- "Available tools:",
2327
- toolsDesc,
2328
- "",
2329
- "Node types: eval (expression), set (variable), get (variable), if (condition), while (loop), call (tool), http (request).",
2330
- "Reference syntax: $var.name, $nodes.id.output, $nodes.id.output.field, $input.field",
2331
- "Output ONLY valid JSON. No explanation, no markdown."
2332
- ].filter(Boolean).join("\n"),
2333
- messages: [{ role: "user", content: input.goal }]
2334
- });
2370
+ const system = [
2371
+ "You are a workflow generator. Given a user goal and available tools, output a workflow JSON.",
2372
+ "",
2373
+ "Available tools:",
2374
+ toolsDesc,
2375
+ "",
2376
+ "Node types: eval (expression), set (variable), get (variable), if (condition), while (loop), call (tool), http (request).",
2377
+ "Reference syntax: $var.name, $nodes.id.output, $nodes.id.output.field, $input.field",
2378
+ "Output ONLY valid JSON. No explanation, no markdown."
2379
+ ].filter(Boolean).join("\n");
2380
+ const genParams = { system, messages: [{ role: "user", content: input.goal }] };
2381
+ const result = opts.provider ? await opts.provider.generateText(genParams) : await generateText({ ...genParams, model: opts.model });
2335
2382
  const text2 = result.text.trim();
2336
2383
  const jsonStart = text2.indexOf("{");
2337
2384
  const jsonEnd = text2.lastIndexOf("}");
@@ -2339,8 +2386,6 @@ function runWorkflow(opts = {}) {
2339
2386
  const parsed = JSON.parse(text2.slice(jsonStart, jsonEnd + 1));
2340
2387
  nodes = parsed.nodes ?? parsed.workflow?.nodes ?? [];
2341
2388
  if (!Array.isArray(nodes)) throw new Error("Generated workflow has no nodes array");
2342
- } else {
2343
- throw new Error('Provide either "nodes" or a "model" to generate the workflow from "goal"');
2344
2389
  }
2345
2390
  const ctx = {
2346
2391
  variables: /* @__PURE__ */ new Map(),
@@ -2360,6 +2405,54 @@ function runWorkflow(opts = {}) {
2360
2405
  });
2361
2406
  }
2362
2407
 
2408
+ // ai/provider.ts
2409
+ import { createOpenAI } from "@ai-sdk/openai";
2410
+ import {
2411
+ embed as aiEmbed,
2412
+ embedMany as aiEmbedMany,
2413
+ generateText as aiGenerateText,
2414
+ streamText as aiStreamText
2415
+ } from "ai";
2416
+ function aiProvider(options) {
2417
+ const baseURL = options?.baseURL ?? process.env.OPENAI_BASE_URL ?? "http://localhost:11434/v1";
2418
+ const apiKey = options?.apiKey ?? process.env.OPENAI_API_KEY ?? "ollama";
2419
+ const modelName = options?.model ?? process.env.OPENAI_MODEL ?? "qwen3:0.6b";
2420
+ const embedModelName = options?.embeddingModel ?? process.env.OPENAI_EMBEDDING_MODEL ?? "qwen3-embedding:0.6b";
2421
+ const dimension = options?.embeddingDimension ?? parseInt(process.env.EMBEDDING_DIMENSION || "1024", 10);
2422
+ const client = createOpenAI({ baseURL, apiKey });
2423
+ let _model;
2424
+ let _embedModel;
2425
+ return {
2426
+ get dimension() {
2427
+ return dimension;
2428
+ },
2429
+ model(name) {
2430
+ const m = name ?? modelName;
2431
+ if (!_model) _model = client(m);
2432
+ return _model;
2433
+ },
2434
+ embeddingModel(name) {
2435
+ const m = name ?? embedModelName;
2436
+ if (!_embedModel) _embedModel = client.embedding(m);
2437
+ return _embedModel;
2438
+ },
2439
+ async embed(text2) {
2440
+ const result = await aiEmbed({ model: this.embeddingModel(), value: text2 });
2441
+ return result.embedding;
2442
+ },
2443
+ async embedMany(texts) {
2444
+ const result = await aiEmbedMany({ model: this.embeddingModel(), value: texts });
2445
+ return result.embeddings;
2446
+ },
2447
+ generateText(params) {
2448
+ return aiGenerateText({ ...params, model: this.model() });
2449
+ },
2450
+ streamText(params) {
2451
+ return aiStreamText({ ...params, model: this.model() });
2452
+ }
2453
+ };
2454
+ }
2455
+
2363
2456
  // ai-sdk.ts
2364
2457
  import {
2365
2458
  streamText,
@@ -2373,7 +2466,7 @@ import {
2373
2466
  } from "ai";
2374
2467
  import {
2375
2468
  openai,
2376
- createOpenAI
2469
+ createOpenAI as createOpenAI2
2377
2470
  } from "@ai-sdk/openai";
2378
2471
 
2379
2472
  // postgres/client.ts
@@ -3293,6 +3386,226 @@ h2{color:#dc2626}.desc{color:#555}</style>
3293
3386
  return { authorizeHandler, consentHandler, tokenHandler, registerClient, getClient, revokeClient };
3294
3387
  }
3295
3388
 
3389
+ // user/oauth-login.ts
3390
+ import crypto4 from "node:crypto";
3391
+ var BUILTIN_PROVIDERS = {
3392
+ google: {
3393
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
3394
+ tokenUrl: "https://oauth2.googleapis.com/token",
3395
+ userUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
3396
+ scope: "openid email profile",
3397
+ parseUser: (data) => ({
3398
+ id: data.id,
3399
+ email: data.email,
3400
+ name: data.name,
3401
+ avatarUrl: data.picture
3402
+ })
3403
+ },
3404
+ github: {
3405
+ authUrl: "https://github.com/login/oauth/authorize",
3406
+ tokenUrl: "https://github.com/login/oauth/access_token",
3407
+ userUrl: "https://api.github.com/user",
3408
+ scope: "read:user user:email",
3409
+ parseUser: (data) => ({
3410
+ id: String(data.id),
3411
+ email: data.email ?? "",
3412
+ name: data.name ?? data.login,
3413
+ avatarUrl: data.avatar_url
3414
+ })
3415
+ }
3416
+ };
3417
+ function registerOAuthLoginRoutes(router, deps, providers) {
3418
+ const { sql: sql2, providerTable, signToken, redirectUrl } = deps;
3419
+ let tableReady = null;
3420
+ async function ensureTable() {
3421
+ if (tableReady) return tableReady;
3422
+ tableReady = (async () => {
3423
+ await sql2.unsafe(`
3424
+ CREATE TABLE IF NOT EXISTS ${escapeIdent(providerTable)} (
3425
+ id SERIAL PRIMARY KEY,
3426
+ user_id INTEGER NOT NULL REFERENCES ${escapeIdent(deps.usersTable)}(id) ON DELETE CASCADE,
3427
+ provider TEXT NOT NULL,
3428
+ provider_id TEXT NOT NULL,
3429
+ email TEXT NOT NULL DEFAULT '',
3430
+ name TEXT NOT NULL DEFAULT '',
3431
+ avatar_url TEXT NOT NULL DEFAULT '',
3432
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3433
+ UNIQUE(provider, provider_id)
3434
+ )
3435
+ `);
3436
+ await sql2.unsafe(`
3437
+ CREATE INDEX IF NOT EXISTS ${escapeIdent(providerTable + "_user_idx")}
3438
+ ON ${escapeIdent(providerTable)}(user_id)
3439
+ `);
3440
+ })();
3441
+ return tableReady;
3442
+ }
3443
+ async function findUserByProvider(provider, providerId) {
3444
+ const [row] = await sql2.unsafe(
3445
+ `SELECT * FROM ${escapeIdent(providerTable)} WHERE provider = $1 AND provider_id = $2 LIMIT 1`,
3446
+ [provider, providerId]
3447
+ );
3448
+ return row ?? null;
3449
+ }
3450
+ async function linkProvider(userId, provider, providerId, email, name, avatarUrl) {
3451
+ await sql2.unsafe(
3452
+ `INSERT INTO ${escapeIdent(providerTable)} (user_id, provider, provider_id, email, name, avatar_url)
3453
+ VALUES ($1, $2, $3, $4, $5, $6)
3454
+ ON CONFLICT (provider, provider_id) DO NOTHING`,
3455
+ [userId, provider, providerId, email, name, avatarUrl]
3456
+ );
3457
+ }
3458
+ async function findOrCreateUser(provider, providerId, email, name, avatarUrl) {
3459
+ const link = await findUserByProvider(provider, providerId);
3460
+ if (link) {
3461
+ const user2 = await deps.findUserById(link.user_id);
3462
+ if (user2) return user2;
3463
+ }
3464
+ if (email) {
3465
+ const existingUser = await deps.findUserByEmail(email);
3466
+ if (existingUser) {
3467
+ await linkProvider(existingUser.id, provider, providerId, email, name, avatarUrl);
3468
+ return existingUser;
3469
+ }
3470
+ }
3471
+ const newUser = await deps.createPlaceholderUser(
3472
+ email || `${provider}_${providerId}@oauth.local`,
3473
+ name || provider
3474
+ );
3475
+ await linkProvider(newUser.id, provider, providerId, email, name, avatarUrl);
3476
+ return newUser;
3477
+ }
3478
+ function getProviderMeta(providerName) {
3479
+ const config = providers[providerName];
3480
+ if (!config) return null;
3481
+ const builtin = BUILTIN_PROVIDERS[providerName];
3482
+ const parseUser = config.parseUser ?? builtin?.parseUser;
3483
+ if (!parseUser) return null;
3484
+ const meta = {
3485
+ authUrl: config.authUrl ?? builtin?.authUrl ?? "",
3486
+ tokenUrl: config.tokenUrl ?? builtin?.tokenUrl ?? "",
3487
+ userUrl: config.userUrl ?? builtin?.userUrl ?? "",
3488
+ scope: config.scope ?? builtin?.scope ?? "openid",
3489
+ parseUser
3490
+ };
3491
+ if (!meta.authUrl || !meta.tokenUrl || !meta.userUrl) return null;
3492
+ return { config, meta };
3493
+ }
3494
+ router.get("/auth/:provider", async (req, ctx) => {
3495
+ await ensureTable();
3496
+ const providerName = ctx.params.provider;
3497
+ const resolved = getProviderMeta(providerName);
3498
+ if (!resolved) {
3499
+ return Response.json({ error: `Unsupported provider: ${providerName}` }, { status: 400 });
3500
+ }
3501
+ const { config, meta } = resolved;
3502
+ const state = crypto4.randomUUID();
3503
+ const redirectUri = new URL(req.url);
3504
+ redirectUri.pathname = redirectUri.pathname.replace(/\/[^/]+$/, "/") + providerName + "/callback";
3505
+ if (ctx.session) {
3506
+ ctx.session.oauthState = { state, provider: providerName };
3507
+ }
3508
+ const scope = config.scope ?? meta.scope;
3509
+ const params = new URLSearchParams({
3510
+ client_id: config.clientId,
3511
+ redirect_uri: redirectUri.origin + redirectUri.pathname,
3512
+ response_type: "code",
3513
+ scope,
3514
+ state,
3515
+ access_type: "offline",
3516
+ prompt: "consent"
3517
+ });
3518
+ return Response.redirect(`${meta.authUrl}?${params.toString()}`, 302);
3519
+ });
3520
+ router.get("/auth/:provider/callback", async (req, ctx) => {
3521
+ const providerName = ctx.params.provider;
3522
+ const resolved = getProviderMeta(providerName);
3523
+ if (!resolved) {
3524
+ return Response.json({ error: `Unsupported provider: ${providerName}` }, { status: 400 });
3525
+ }
3526
+ const { config, meta } = resolved;
3527
+ const url = new URL(req.url);
3528
+ const code = url.searchParams.get("code");
3529
+ const state = url.searchParams.get("state");
3530
+ if (!code || !state) {
3531
+ return Response.json({ error: "Missing code or state parameter" }, { status: 400 });
3532
+ }
3533
+ const savedState = ctx.session?.oauthState;
3534
+ if (!savedState || savedState.state !== state || savedState.provider !== providerName) {
3535
+ return Response.json({ error: "Invalid state \u2014 possible CSRF attack" }, { status: 403 });
3536
+ }
3537
+ if (ctx.session) delete ctx.session.oauthState;
3538
+ const redirectUri = url.origin + url.pathname.replace(/\/callback$/, "");
3539
+ let tokenRes;
3540
+ try {
3541
+ tokenRes = await fetch(meta.tokenUrl, {
3542
+ method: "POST",
3543
+ headers: { "Content-Type": "application/json", "Accept": "application/json" },
3544
+ body: JSON.stringify({
3545
+ code,
3546
+ client_id: config.clientId,
3547
+ client_secret: config.clientSecret,
3548
+ redirect_uri: redirectUri,
3549
+ grant_type: "authorization_code"
3550
+ })
3551
+ });
3552
+ } catch (err) {
3553
+ console.error(`[oauth] token exchange network error for ${providerName}:`, err.message);
3554
+ return Response.json({ error: "Failed to connect to OAuth provider" }, { status: 502 });
3555
+ }
3556
+ if (!tokenRes.ok) {
3557
+ const errBody = await tokenRes.text();
3558
+ console.error(`[oauth] token exchange failed for ${providerName}:`, errBody);
3559
+ return Response.json({ error: "Failed to exchange authorization code" }, { status: 502 });
3560
+ }
3561
+ const tokenData = await tokenRes.json();
3562
+ const accessToken = tokenData.access_token;
3563
+ if (!accessToken) {
3564
+ return Response.json({ error: "No access_token in response" }, { status: 502 });
3565
+ }
3566
+ let userRes;
3567
+ try {
3568
+ userRes = await fetch(meta.userUrl, { headers: { Authorization: "Bearer " + accessToken } });
3569
+ } catch (err) {
3570
+ console.error("[oauth] user info network error for " + providerName + ":", err.message);
3571
+ return Response.json({ error: "Failed to connect to OAuth provider" }, { status: 502 });
3572
+ }
3573
+ if (!userRes.ok) {
3574
+ return Response.json({ error: "Failed to fetch user profile" }, { status: 502 });
3575
+ }
3576
+ const userData = await userRes.json();
3577
+ const providerUser = meta.parseUser(userData, accessToken);
3578
+ const user2 = await findOrCreateUser(
3579
+ providerName,
3580
+ providerUser.id,
3581
+ providerUser.email,
3582
+ providerUser.name,
3583
+ providerUser.avatarUrl ?? ""
3584
+ );
3585
+ if (!user2) {
3586
+ return Response.json({ error: "Failed to create/link user" }, { status: 500 });
3587
+ }
3588
+ const token = signToken(user2);
3589
+ if (ctx.session) {
3590
+ ctx.session.userId = user2.id;
3591
+ ctx.session.role = user2.role;
3592
+ }
3593
+ const accept = req.headers.get("accept") ?? "";
3594
+ if (accept.includes("application/json")) {
3595
+ return Response.json({
3596
+ token,
3597
+ user: { id: user2.id, email: user2.email, name: user2.name, role: user2.role }
3598
+ });
3599
+ }
3600
+ const finalUrl = new URL(redirectUrl, url.origin);
3601
+ finalUrl.searchParams.set("token", token);
3602
+ return Response.redirect(finalUrl.toString(), 302);
3603
+ });
3604
+ }
3605
+ function escapeIdent(s) {
3606
+ return `"${s.replace(/"/g, '""')}"`;
3607
+ }
3608
+
3296
3609
  // user/client.ts
3297
3610
  var RegisterSchema = z2.object({
3298
3611
  email: z2.string().email(),
@@ -3303,6 +3616,9 @@ var LoginSchema = z2.object({
3303
3616
  email: z2.string().email(),
3304
3617
  password: z2.string().min(1)
3305
3618
  });
3619
+ function escapeIdent2(s) {
3620
+ return `"${s.replace(/"/g, '""')}"`;
3621
+ }
3306
3622
  function hashPassword(password) {
3307
3623
  const salt = randomBytes(16).toString("hex");
3308
3624
  const hash = scryptSync(password, salt, 64).toString("hex");
@@ -3336,6 +3652,25 @@ function user(options) {
3336
3652
  }
3337
3653
  async function migrate() {
3338
3654
  await users.create();
3655
+ if (options.oauthLogin) {
3656
+ await pg.sql.unsafe(`
3657
+ CREATE TABLE IF NOT EXISTS "_auth_providers" (
3658
+ id SERIAL PRIMARY KEY,
3659
+ user_id INTEGER NOT NULL REFERENCES ${escapeIdent2(table)}(id) ON DELETE CASCADE,
3660
+ provider TEXT NOT NULL,
3661
+ provider_id TEXT NOT NULL,
3662
+ email TEXT NOT NULL DEFAULT '',
3663
+ name TEXT NOT NULL DEFAULT '',
3664
+ avatar_url TEXT NOT NULL DEFAULT '',
3665
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3666
+ UNIQUE(provider, provider_id)
3667
+ )
3668
+ `);
3669
+ await pg.sql.unsafe(`
3670
+ CREATE INDEX IF NOT EXISTS "_auth_providers_user_idx"
3671
+ ON "_auth_providers"(user_id)
3672
+ `);
3673
+ }
3339
3674
  if (!oauth2Enabled) return;
3340
3675
  const clients3 = pg.table("_oauth2_clients", {
3341
3676
  id: serial("id").primaryKey(),
@@ -3389,6 +3724,11 @@ function user(options) {
3389
3724
  async function findById(id2) {
3390
3725
  return await users.read(id2);
3391
3726
  }
3727
+ async function createPlaceholderUser(email, name) {
3728
+ const randomPassword = randomBytes(32).toString("hex");
3729
+ const row = await users.insert({ email, password: randomPassword, name });
3730
+ return row;
3731
+ }
3392
3732
  async function register(data) {
3393
3733
  const { email, password, name } = RegisterSchema.parse(data);
3394
3734
  const existing = await findByEmail(email);
@@ -3505,6 +3845,20 @@ function user(options) {
3505
3845
  return r2;
3506
3846
  }
3507
3847
  const r = router();
3848
+ if (options.oauthLogin) {
3849
+ registerOAuthLoginRoutes(r, {
3850
+ sql: pg.sql,
3851
+ jwtSecret: secret,
3852
+ expiresIn,
3853
+ usersTable: table,
3854
+ providerTable: "_auth_providers",
3855
+ redirectUrl: options.oauthLogin.redirectUrl || "/",
3856
+ signToken,
3857
+ createPlaceholderUser,
3858
+ findUserById: findById,
3859
+ findUserByEmail: findByEmail
3860
+ }, options.oauthLogin.providers);
3861
+ }
3508
3862
  const mod = r;
3509
3863
  mod.middleware = middleware;
3510
3864
  mod.migrate = migrate;
@@ -3542,30 +3896,9 @@ function redis(opts) {
3542
3896
 
3543
3897
  // queue/index.ts
3544
3898
  import { Redis as IORedis2 } from "ioredis";
3545
- import crypto4 from "node:crypto";
3546
- function cronNext(expr, from = /* @__PURE__ */ new Date()) {
3547
- const parts = expr.trim().split(/\s+/);
3548
- if (parts.length !== 5) throw new Error(`Invalid cron expression "${expr}": expected 5 fields`);
3549
- const fields = parts.map((f, i) => {
3550
- const ranges = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
3551
- const [min, max] = ranges[i];
3552
- return parseField(f, min, max);
3553
- });
3554
- let candidate = new Date(from.getTime() + 6e4);
3555
- candidate.setSeconds(0, 0);
3556
- for (let i = 0; i < 525600; i++) {
3557
- const m = candidate.getMonth() + 1;
3558
- const d = candidate.getDate();
3559
- const h = candidate.getHours();
3560
- const min = candidate.getMinutes();
3561
- const dw = candidate.getDay();
3562
- if (fields[4].has(dw) && fields[3].has(m) && fields[2].has(d) && fields[1].has(h) && fields[0].has(min)) {
3563
- return candidate.getTime();
3564
- }
3565
- candidate.setTime(candidate.getTime() + 6e4);
3566
- }
3567
- throw new Error(`No future date found for cron expression "${expr}"`);
3568
- }
3899
+ import crypto5 from "node:crypto";
3900
+
3901
+ // cron-utils.ts
3569
3902
  function parseField(field, min, max) {
3570
3903
  const values = /* @__PURE__ */ new Set();
3571
3904
  for (const part of field.split(",")) {
@@ -3574,19 +3907,23 @@ function parseField(field, min, max) {
3574
3907
  } else if (part.includes("/")) {
3575
3908
  const [range, stepStr] = part.split("/");
3576
3909
  const step = parseInt(stepStr, 10);
3910
+ if (isNaN(step) || step < 1) throw new Error(`Invalid cron step: ${part}`);
3577
3911
  let start = min;
3578
3912
  let end = max;
3579
3913
  if (range !== "*") {
3580
- const parts = range.split("-");
3581
- start = parseInt(parts[0], 10);
3582
- end = parts.length > 1 ? parseInt(parts[1], 10) : max;
3914
+ const rangeParts = range.split("-");
3915
+ start = parseInt(rangeParts[0], 10);
3916
+ end = rangeParts.length > 1 ? parseInt(rangeParts[1], 10) : max;
3583
3917
  }
3584
3918
  for (let i = start; i <= end; i += step) values.add(i);
3585
3919
  } else if (part.includes("-")) {
3586
3920
  const [s, e] = part.split("-").map(Number);
3921
+ if (isNaN(s) || isNaN(e)) throw new Error(`Invalid cron range: ${part}`);
3587
3922
  for (let i = s; i <= e; i++) values.add(i);
3588
3923
  } else {
3589
- values.add(parseInt(part, 10));
3924
+ const val = parseInt(part, 10);
3925
+ if (isNaN(val)) throw new Error(`Invalid cron value: ${part}`);
3926
+ values.add(val);
3590
3927
  }
3591
3928
  }
3592
3929
  const result = /* @__PURE__ */ new Set();
@@ -3595,41 +3932,333 @@ function parseField(field, min, max) {
3595
3932
  }
3596
3933
  return result;
3597
3934
  }
3935
+ var FIELD_RANGES = [
3936
+ [0, 59],
3937
+ // minute
3938
+ [0, 23],
3939
+ // hour
3940
+ [1, 31],
3941
+ // day of month
3942
+ [1, 12],
3943
+ // month
3944
+ [0, 6]
3945
+ // day of week (0=Sunday)
3946
+ ];
3947
+ function parsePattern(pattern) {
3948
+ const fields = pattern.trim().split(/\s+/);
3949
+ if (fields.length !== 5) {
3950
+ throw new Error(
3951
+ `Invalid cron pattern "${pattern}": expected 5 fields, got ${fields.length}`
3952
+ );
3953
+ }
3954
+ return fields.map((f, i) => parseField(f, FIELD_RANGES[i][0], FIELD_RANGES[i][1]));
3955
+ }
3956
+ function matches(fields, date) {
3957
+ return fields[0].has(date.getMinutes()) && fields[1].has(date.getHours()) && fields[2].has(date.getDate()) && fields[3].has(date.getMonth() + 1) && fields[4].has(date.getDay());
3958
+ }
3959
+ function cronNext(expr, from = /* @__PURE__ */ new Date()) {
3960
+ const fields = parsePattern(expr);
3961
+ let candidate = new Date(from.getTime() + 6e4);
3962
+ candidate.setSeconds(0, 0);
3963
+ for (let i = 0; i < 525600; i++) {
3964
+ if (fields[4].has(candidate.getDay()) && fields[3].has(candidate.getMonth() + 1) && fields[2].has(candidate.getDate()) && fields[1].has(candidate.getHours()) && fields[0].has(candidate.getMinutes())) {
3965
+ return candidate.getTime();
3966
+ }
3967
+ candidate.setTime(candidate.getTime() + 6e4);
3968
+ }
3969
+ throw new Error(`No future date found for cron expression "${expr}"`);
3970
+ }
3971
+
3972
+ // queue/index.ts
3598
3973
  function queue(opts) {
3599
- const redis2 = opts?.redis ?? new IORedis2(opts?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379");
3600
- const prefix = opts?.prefix ?? "queue";
3974
+ const store2 = opts?.store ?? "memory";
3975
+ if (store2 === "redis") return createRedisQueue(opts);
3976
+ if (store2 === "pg") return createPgQueue(opts);
3977
+ return createMemoryQueue(opts);
3978
+ }
3979
+ function escapeIdent3(s) {
3980
+ return '"' + s.replace(/"/g, '""') + '"';
3981
+ }
3982
+ function attachCron(q, handlers) {
3983
+ ;
3984
+ q.cron = function(pattern, handler) {
3985
+ const id2 = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto5.randomUUID().slice(0, 8);
3986
+ q.process(id2, async () => {
3987
+ await handler();
3988
+ });
3989
+ q.add(id2, {}, { schedule: pattern });
3990
+ return { stop: () => handlers.delete(id2) };
3991
+ };
3992
+ }
3993
+ function createMemoryQueue(opts) {
3601
3994
  const pollInterval = opts?.pollInterval ?? 200;
3602
3995
  const handlers = /* @__PURE__ */ new Map();
3996
+ const jobs = [];
3997
+ const failed = [];
3998
+ const MAX_FAILED = 1e3;
3603
3999
  let running = false;
3604
4000
  let pollTimer = null;
3605
- let epoch = 0;
3606
4001
  let _processed = 0;
3607
4002
  let _failed = 0;
3608
- const jobKey = `${prefix}:jobs`;
3609
- const failedKey = `${prefix}:failed`;
3610
- const MAX_FAILED = 1e3;
4003
+ let inflight = 0;
4004
+ const MAX_CONCURRENT = 16;
4005
+ function insertJob(job) {
4006
+ let i = 0;
4007
+ while (i < jobs.length && jobs[i].runAt <= job.runAt) i++;
4008
+ jobs.splice(i, 0, job);
4009
+ }
4010
+ async function execute(job, handler) {
4011
+ inflight++;
4012
+ try {
4013
+ await handler(job);
4014
+ _processed++;
4015
+ } catch (e) {
4016
+ _failed++;
4017
+ failed.unshift({ ...job, error: e.message, failedAt: Date.now() });
4018
+ if (failed.length > MAX_FAILED) failed.length = MAX_FAILED;
4019
+ } finally {
4020
+ inflight--;
4021
+ }
4022
+ if (job.schedule) {
4023
+ try {
4024
+ insertJob({ ...job, id: crypto5.randomUUID(), runAt: cronNext(job.schedule), createdAt: Date.now() });
4025
+ } catch (e) {
4026
+ console.error("[queue] cron re-queue failed:", e.message);
4027
+ }
4028
+ }
4029
+ }
4030
+ async function poll() {
4031
+ if (!running) return;
4032
+ const now = Date.now();
4033
+ while (running && inflight < MAX_CONCURRENT && jobs.length > 0 && jobs[0].runAt <= now) {
4034
+ const job = jobs.shift();
4035
+ const handler = handlers.get(job.type);
4036
+ if (handler) execute(job, handler);
4037
+ }
4038
+ if (running) pollTimer = setTimeout(poll, pollInterval);
4039
+ }
3611
4040
  const mw = ((req, ctx, next) => {
3612
4041
  ctx.queue = q;
3613
4042
  return next(req, ctx);
3614
4043
  });
3615
4044
  const q = mw;
4045
+ mw.add = function add(type, payload, opts2) {
4046
+ const id2 = crypto5.randomUUID();
4047
+ let runAt;
4048
+ if (opts2?.schedule) {
4049
+ try {
4050
+ const f = parsePattern(opts2.schedule);
4051
+ runAt = matches(f, /* @__PURE__ */ new Date()) ? Date.now() : cronNext(opts2.schedule);
4052
+ } catch {
4053
+ runAt = cronNext(opts2.schedule);
4054
+ }
4055
+ } else if (opts2?.delay) {
4056
+ runAt = Date.now() + opts2.delay;
4057
+ } else {
4058
+ runAt = Date.now();
4059
+ }
4060
+ const job = { id: id2, type, payload, createdAt: Date.now(), runAt };
4061
+ if (opts2?.schedule) job.schedule = opts2.schedule;
4062
+ insertJob(job);
4063
+ return Promise.resolve(id2);
4064
+ };
4065
+ mw.process = function process2(type, handler) {
4066
+ handlers.set(type, handler);
4067
+ };
4068
+ mw.run = async function run() {
4069
+ if (running) return;
4070
+ running = true;
4071
+ poll();
4072
+ };
4073
+ mw.stop = function stop2() {
4074
+ running = false;
4075
+ if (pollTimer) {
4076
+ clearTimeout(pollTimer);
4077
+ pollTimer = null;
4078
+ }
4079
+ };
4080
+ mw.close = async function close() {
4081
+ mw.stop();
4082
+ while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
4083
+ };
4084
+ mw.jobs = async function jobs2(limit) {
4085
+ return jobs2.slice(0, limit ?? 50);
4086
+ };
4087
+ mw.failedJobs = async function failedJobs(limit) {
4088
+ return failed.slice(0, limit ?? 50);
4089
+ };
4090
+ mw.retryFailed = async function retry(jobId) {
4091
+ const idx = failed.findIndex((j) => j.id === jobId);
4092
+ if (idx < 0) return false;
4093
+ const [entry] = failed.splice(idx, 1);
4094
+ _failed--;
4095
+ insertJob({ ...entry, runAt: Date.now() });
4096
+ return true;
4097
+ };
4098
+ mw.retryAllFailed = async function retryAll(type) {
4099
+ let count = 0;
4100
+ for (let i = failed.length - 1; i >= 0; i--) {
4101
+ if (type && failed[i].type !== type) continue;
4102
+ const [entry] = failed.splice(i, 1);
4103
+ _failed--;
4104
+ insertJob({ ...entry, runAt: Date.now() });
4105
+ count++;
4106
+ }
4107
+ return count;
4108
+ };
4109
+ mw.dashboard = function dashboard() {
4110
+ return buildDashboard(q);
4111
+ };
4112
+ mw.stats = () => ({ running, inflight, processed: _processed, failed: _failed, handlers: handlers.size, maxConcurrent: MAX_CONCURRENT });
4113
+ attachCron(q, handlers);
4114
+ return q;
4115
+ }
4116
+ function createPgQueue(opts) {
4117
+ const sql2 = opts.pg.sql;
4118
+ const pollInterval = opts?.pollInterval ?? 200;
4119
+ const table = (opts?.prefix ?? "queue") + "_jobs";
4120
+ const handlers = /* @__PURE__ */ new Map();
4121
+ let running = false, pollTimer = null;
4122
+ let _processed = 0, _failed = 0, inflight = 0, ready = false;
3616
4123
  const MAX_CONCURRENT = 16;
3617
- let inflight = 0;
3618
- async function processJob(job, jobHandler) {
4124
+ const MAX_FAILED = 1e3;
4125
+ async function ensureTable() {
4126
+ if (ready) return;
4127
+ await sql2.unsafe(`CREATE TABLE IF NOT EXISTS ${escapeIdent3(table)} (id UUID PRIMARY KEY, type TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}', run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), schedule TEXT, status TEXT NOT NULL DEFAULT 'pending', error TEXT, failed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW())`);
4128
+ await sql2.unsafe(`CREATE INDEX IF NOT EXISTS ${escapeIdent3(table + "_run_at_idx")} ON ${escapeIdent3(table)} (run_at, status)`);
4129
+ ready = true;
4130
+ }
4131
+ async function processJob(job, handler) {
3619
4132
  inflight++;
3620
4133
  try {
3621
- await jobHandler(job);
4134
+ await handler(job);
3622
4135
  _processed++;
4136
+ await sql2.unsafe(`DELETE FROM ${escapeIdent3(table)} WHERE id = $1`, [job.id]);
3623
4137
  } catch (e) {
3624
4138
  _failed++;
3625
- const errMsg = e.message;
3626
- console.error("[queue] handler error:", errMsg);
3627
- const failedEntry = JSON.stringify({
3628
- ...job,
3629
- error: errMsg,
3630
- failedAt: Date.now()
3631
- });
3632
- await redis2.lpush(failedKey, failedEntry);
4139
+ const msg = e.message;
4140
+ console.error("[queue] handler error:", msg);
4141
+ await sql2.unsafe(`UPDATE ${escapeIdent3(table)} SET status = 'failed', error = $2, failed_at = NOW() WHERE id = $1`, [job.id, msg]);
4142
+ } finally {
4143
+ inflight--;
4144
+ }
4145
+ if (job.schedule) {
4146
+ try {
4147
+ const nextRun = cronNext(job.schedule);
4148
+ await sql2.unsafe(`INSERT INTO ${escapeIdent3(table)} (id, type, payload, run_at, schedule) VALUES ($1, $2, $3::jsonb, $4, $5)`, [crypto5.randomUUID(), job.type, JSON.stringify(job.payload), new Date(nextRun).toISOString(), job.schedule]);
4149
+ } catch (e) {
4150
+ console.error("[queue] cron re-queue failed:", e.message);
4151
+ }
4152
+ }
4153
+ }
4154
+ async function poll() {
4155
+ if (!running) return;
4156
+ try {
4157
+ while (running && inflight < MAX_CONCURRENT) {
4158
+ const rows = await sql2.unsafe(`UPDATE ${escapeIdent3(table)} SET status = 'running' WHERE id = (SELECT id FROM ${escapeIdent3(table)} WHERE run_at <= NOW() AND status = 'pending' ORDER BY run_at LIMIT 1 FOR UPDATE SKIP LOCKED) RETURNING *`);
4159
+ if (rows.length === 0) break;
4160
+ const row = rows[0];
4161
+ const job = { id: row.id, type: row.type, payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload, createdAt: new Date(row.created_at).getTime(), runAt: new Date(row.run_at).getTime(), schedule: row.schedule || void 0 };
4162
+ const handler = handlers.get(job.type);
4163
+ if (handler) processJob(job, handler);
4164
+ }
4165
+ } catch (e) {
4166
+ const msg = e.message;
4167
+ if (msg.includes("CONNECTION_ENDED") || msg.includes("Connection terminated")) {
4168
+ running = false;
4169
+ return;
4170
+ }
4171
+ console.error("[queue] poll error:", msg);
4172
+ }
4173
+ if (running) pollTimer = setTimeout(poll, pollInterval);
4174
+ }
4175
+ const mw = ((req, ctx, next) => {
4176
+ ctx.queue = q;
4177
+ return next(req, ctx);
4178
+ });
4179
+ const q = mw;
4180
+ mw.add = function add(type, payload, opts2) {
4181
+ return (async () => {
4182
+ const id2 = crypto5.randomUUID();
4183
+ let runAt;
4184
+ if (opts2?.schedule) {
4185
+ try {
4186
+ const f = parsePattern(opts2.schedule);
4187
+ runAt = matches(f, /* @__PURE__ */ new Date()) ? /* @__PURE__ */ new Date() : new Date(cronNext(opts2.schedule));
4188
+ } catch {
4189
+ runAt = new Date(cronNext(opts2.schedule));
4190
+ }
4191
+ } else if (opts2?.delay) {
4192
+ runAt = new Date(Date.now() + opts2.delay);
4193
+ } else {
4194
+ runAt = /* @__PURE__ */ new Date();
4195
+ }
4196
+ await sql2.unsafe(`INSERT INTO ${escapeIdent3(table)} (id, type, payload, run_at, schedule) VALUES ($1, $2, $3::jsonb, $4, $5)`, [id2, type, JSON.stringify(payload), runAt.toISOString(), opts2?.schedule || null]);
4197
+ return id2;
4198
+ })();
4199
+ };
4200
+ mw.process = function process2(type, handler) {
4201
+ handlers.set(type, handler);
4202
+ };
4203
+ mw.migrate = ensureTable;
4204
+ mw.run = async function run() {
4205
+ if (running) return;
4206
+ await ensureTable();
4207
+ running = true;
4208
+ poll();
4209
+ };
4210
+ mw.stop = function stop2() {
4211
+ running = false;
4212
+ if (pollTimer) {
4213
+ clearTimeout(pollTimer);
4214
+ pollTimer = null;
4215
+ }
4216
+ };
4217
+ mw.close = async function close() {
4218
+ mw.stop();
4219
+ while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
4220
+ };
4221
+ mw.jobs = async function jobs(limit) {
4222
+ const rows = await sql2.unsafe(`SELECT * FROM ${escapeIdent3(table)} WHERE status = 'pending' ORDER BY run_at LIMIT $1`, [limit ?? 50]);
4223
+ return rows.map((r) => ({ id: r.id, type: r.type, payload: typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload, createdAt: new Date(r.created_at).getTime(), runAt: new Date(r.run_at).getTime(), schedule: r.schedule || void 0 }));
4224
+ };
4225
+ mw.failedJobs = async function failedJobs(limit) {
4226
+ const rows = await sql2.unsafe(`SELECT * FROM ${escapeIdent3(table)} WHERE status = 'failed' ORDER BY failed_at DESC LIMIT $1`, [limit ?? 50]);
4227
+ return rows.map((r) => ({ id: r.id, type: r.type, payload: typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload, createdAt: new Date(r.created_at).getTime(), runAt: new Date(r.run_at).getTime(), schedule: r.schedule || void 0, error: r.error || "", failedAt: new Date(r.failed_at).getTime() }));
4228
+ };
4229
+ mw.retryFailed = async function retryFailed(jobId) {
4230
+ const result = await sql2.unsafe(`UPDATE ${escapeIdent3(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE id = $1 AND status = 'failed' RETURNING id`, [jobId]);
4231
+ return result.length > 0;
4232
+ };
4233
+ mw.retryAllFailed = async function retryAllFailed(type) {
4234
+ const result = await sql2.unsafe(type ? `UPDATE ${escapeIdent3(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE status = 'failed' AND type = $1 RETURNING id` : `UPDATE ${escapeIdent3(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE status = 'failed' RETURNING id`, type ? [type] : []);
4235
+ return result.length;
4236
+ };
4237
+ mw.dashboard = function dashboard() {
4238
+ return buildDashboard(q);
4239
+ };
4240
+ mw.stats = () => ({ running, inflight, processed: _processed, failed: _failed, handlers: handlers.size, maxConcurrent: MAX_CONCURRENT });
4241
+ attachCron(q, handlers);
4242
+ return q;
4243
+ }
4244
+ function createRedisQueue(opts) {
4245
+ const redis2 = opts?.redis ?? new IORedis2(opts?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379");
4246
+ const prefix = opts?.prefix ?? "queue";
4247
+ const pollInterval = opts?.pollInterval ?? 200;
4248
+ const handlers = /* @__PURE__ */ new Map();
4249
+ let running = false, pollTimer = null, epoch = 0;
4250
+ let _processed = 0, _failed = 0, inflight = 0;
4251
+ const jobKey = prefix + ":jobs", failedKey = prefix + ":failed", MAX_FAILED = 1e3, MAX_CONCURRENT = 16;
4252
+ async function processJob(job, handler) {
4253
+ inflight++;
4254
+ try {
4255
+ await handler(job);
4256
+ _processed++;
4257
+ } catch (e) {
4258
+ _failed++;
4259
+ const msg = e.message;
4260
+ console.error("[queue] handler error:", msg);
4261
+ await redis2.lpush(failedKey, JSON.stringify({ ...job, error: msg, failedAt: Date.now() }));
3633
4262
  await redis2.ltrim(failedKey, 0, MAX_FAILED - 1);
3634
4263
  } finally {
3635
4264
  inflight--;
@@ -3637,8 +4266,7 @@ function queue(opts) {
3637
4266
  if (job.schedule) {
3638
4267
  try {
3639
4268
  const nextRun = cronNext(job.schedule);
3640
- const nextJob = { ...job, id: crypto4.randomUUID(), runAt: nextRun, createdAt: Date.now() };
3641
- await redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob));
4269
+ await redis2.zadd(jobKey, nextRun, JSON.stringify({ ...job, id: crypto5.randomUUID(), runAt: nextRun, createdAt: Date.now() }));
3642
4270
  } catch (e) {
3643
4271
  console.error("[queue] cron re-queue failed:", e.message);
3644
4272
  }
@@ -3652,8 +4280,7 @@ function queue(opts) {
3652
4280
  while (running && inflight < MAX_CONCURRENT) {
3653
4281
  const result = await redis2.zpopmin(jobKey);
3654
4282
  if (result.length < 2) break;
3655
- const raw = result[0];
3656
- const score = parseInt(result[1], 10);
4283
+ const raw = result[0], score = parseInt(result[1], 10);
3657
4284
  if (score > now) {
3658
4285
  await redis2.zadd(jobKey, score, raw);
3659
4286
  break;
@@ -3664,20 +4291,21 @@ function queue(opts) {
3664
4291
  } catch {
3665
4292
  continue;
3666
4293
  }
3667
- const jobHandler = handlers.get(job.type);
3668
- if (jobHandler) {
3669
- processJob(job, jobHandler);
3670
- }
4294
+ const handler = handlers.get(job.type);
4295
+ if (handler) processJob(job, handler);
3671
4296
  }
3672
4297
  } catch (e) {
3673
4298
  console.error("[queue] poll error:", e.message);
3674
4299
  }
3675
- if (running && currentEpoch === epoch) {
3676
- pollTimer = setTimeout(poll, pollInterval);
3677
- }
4300
+ if (running && currentEpoch === epoch) pollTimer = setTimeout(poll, pollInterval);
3678
4301
  }
4302
+ const mw = ((req, ctx, next) => {
4303
+ ctx.queue = q;
4304
+ return next(req, ctx);
4305
+ });
4306
+ const q = mw;
3679
4307
  mw.add = function add(type, payload, opts2) {
3680
- const id2 = crypto4.randomUUID();
4308
+ const id2 = crypto5.randomUUID();
3681
4309
  let runAt;
3682
4310
  if (opts2?.schedule) {
3683
4311
  runAt = cronNext(opts2.schedule);
@@ -3751,8 +4379,8 @@ function queue(opts) {
3751
4379
  return false;
3752
4380
  };
3753
4381
  mw.retryAllFailed = async function retryAllFailed(type) {
3754
- const raw = await redis2.lrange(failedKey, 0, -1);
3755
4382
  let count = 0;
4383
+ const raw = await redis2.lrange(failedKey, 0, -1);
3756
4384
  for (const entry of raw) {
3757
4385
  try {
3758
4386
  const job = JSON.parse(entry);
@@ -3770,52 +4398,43 @@ function queue(opts) {
3770
4398
  return count;
3771
4399
  };
3772
4400
  mw.dashboard = function dashboard() {
3773
- const r = new Router();
3774
- r.get("/", async (req, ctx) => {
3775
- const s = q.stats();
3776
- const pending = await q.jobs(100);
3777
- const byType = {};
3778
- for (const job of pending) {
3779
- if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
3780
- byType[job.type].pending++;
3781
- }
3782
- const failed = await q.failedJobs(1e3);
3783
- for (const job of failed) {
3784
- if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
3785
- byType[job.type].failed++;
3786
- }
3787
- return Response.json({
3788
- stats: s,
3789
- types: byType,
3790
- failedCount: failed.length
3791
- });
3792
- });
3793
- r.get("/:type/failed", async (req, ctx) => {
3794
- const failed = await q.failedJobs(100);
3795
- const filtered = failed.filter((j) => j.type === ctx.params.type);
3796
- return Response.json({ jobs: filtered, count: filtered.length });
3797
- });
3798
- r.post("/:type/retry", async (req, ctx) => {
3799
- const count = await q.retryAllFailed(ctx.params.type);
3800
- return Response.json({ retried: count });
3801
- });
3802
- r.post("/retry/:id", async (req, ctx) => {
3803
- const ok = await q.retryFailed(ctx.params.id);
3804
- if (!ok) return new Response("Job not found", { status: 404 });
3805
- return Response.json({ retried: true });
3806
- });
3807
- return r;
4401
+ return buildDashboard(q);
3808
4402
  };
3809
- mw.stats = () => ({
3810
- running,
3811
- inflight,
3812
- processed: _processed,
3813
- failed: _failed,
3814
- handlers: handlers.size,
3815
- maxConcurrent: MAX_CONCURRENT
3816
- });
4403
+ mw.stats = () => ({ running, inflight, processed: _processed, failed: _failed, handlers: handlers.size, maxConcurrent: MAX_CONCURRENT });
4404
+ attachCron(q, handlers);
3817
4405
  return q;
3818
4406
  }
4407
+ function buildDashboard(q) {
4408
+ const r = new Router();
4409
+ r.get("/", async () => {
4410
+ const s = q.stats();
4411
+ const pending = await q.jobs(100);
4412
+ const byType = {};
4413
+ for (const j of pending) {
4414
+ if (!byType[j.type]) byType[j.type] = { pending: 0, failed: 0 };
4415
+ byType[j.type].pending++;
4416
+ }
4417
+ const failed = await q.failedJobs(1e3);
4418
+ for (const j of failed) {
4419
+ if (!byType[j.type]) byType[j.type] = { pending: 0, failed: 0 };
4420
+ byType[j.type].failed++;
4421
+ }
4422
+ return Response.json({ stats: s, types: byType, failedCount: failed.length });
4423
+ });
4424
+ r.get("/:type/failed", async (req, ctx) => {
4425
+ const failed = await q.failedJobs(100);
4426
+ return Response.json({ jobs: failed.filter((j) => j.type === ctx.params.type) });
4427
+ });
4428
+ r.post("/:type/retry", async (req, ctx) => {
4429
+ return Response.json({ retried: await q.retryAllFailed(ctx.params.type) });
4430
+ });
4431
+ r.post("/retry/:id", async (req, ctx) => {
4432
+ const ok = await q.retryFailed(ctx.params.id);
4433
+ if (!ok) return new Response("Not found", { status: 404 });
4434
+ return Response.json({ ok: true });
4435
+ });
4436
+ return r;
4437
+ }
3819
4438
 
3820
4439
  // tenant/rest.ts
3821
4440
  import { z as z3 } from "zod";
@@ -4723,9 +5342,6 @@ function tenant(options) {
4723
5342
  return mod;
4724
5343
  }
4725
5344
 
4726
- // agent/client.ts
4727
- import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
4728
-
4729
5345
  // agent/rest.ts
4730
5346
  function buildRouter2(deps) {
4731
5347
  const { agents: agentsTable, runs: runsTable, knowledge, runner } = deps;
@@ -4879,12 +5495,10 @@ function buildRouter2(deps) {
4879
5495
  }
4880
5496
 
4881
5497
  // agent/run.ts
4882
- import { streamText as streamText2, generateText as generateText3, embed as embed2 } from "ai";
4883
5498
  import { z as z4 } from "zod";
4884
- function hasKnowledgeDocs(sql2, agentId) {
4885
- return sql2`SELECT 1 FROM "_knowledge_documents" WHERE agent_id = ${agentId} LIMIT 1`.then((r) => r.length > 0);
4886
- }
4887
- function chunkContent(content, chunkSize = 512, overlap = 64) {
5499
+
5500
+ // ai/utils.ts
5501
+ function chunkContent(content, chunkSize, overlap) {
4888
5502
  const paragraphs = content.split(/\n\n+/);
4889
5503
  const chunks = [];
4890
5504
  let current = "";
@@ -4898,8 +5512,13 @@ function chunkContent(content, chunkSize = 512, overlap = 64) {
4898
5512
  if (current) chunks.push(current);
4899
5513
  return chunks;
4900
5514
  }
4901
- async function searchKnowledge(sql2, embedModel, agentId, query, limit = 5) {
4902
- const { embedding } = await embed2({ model: embedModel, value: query });
5515
+
5516
+ // agent/run.ts
5517
+ function hasKnowledgeDocs(sql2, agentId) {
5518
+ return sql2`SELECT 1 FROM "_knowledge_documents" WHERE agent_id = ${agentId} LIMIT 1`.then((r) => r.length > 0);
5519
+ }
5520
+ async function searchKnowledge(sql2, provider, agentId, query, limit = 5) {
5521
+ const embedding = await provider.embed(query);
4903
5522
  const vec = `[${embedding.join(",")}]`;
4904
5523
  const docs = await sql2.unsafe(
4905
5524
  `SELECT id, title, content, metadata, embedding <=> $1::vector AS _score FROM "_knowledge_documents" WHERE agent_id = $2 ORDER BY embedding <=> $1::vector LIMIT $3`,
@@ -4912,7 +5531,7 @@ async function loadAgent(agents, agentId) {
4912
5531
  return row ?? null;
4913
5532
  }
4914
5533
  function createRunner(deps) {
4915
- const { sql: sql2, agents, runs, getModel, getEmbeddingModel, userTools } = deps;
5534
+ const { sql: sql2, agents, runs, provider, modelName, userTools } = deps;
4916
5535
  function truncate(s, max = 200) {
4917
5536
  return s.length > max ? s.slice(0, max) + "..." : s;
4918
5537
  }
@@ -4937,8 +5556,6 @@ function createRunner(deps) {
4937
5556
  async function run(agentId, params) {
4938
5557
  const agent2 = await loadAgent(agents, agentId);
4939
5558
  if (!agent2 || !agent2.active) throw new Error("Agent not found or inactive");
4940
- const model = getModel();
4941
- const embedModel = getEmbeddingModel();
4942
5559
  const start = Date.now();
4943
5560
  const hasKB = await hasKnowledgeDocs(sql2, agentId);
4944
5561
  const messages2 = params.messages ?? [];
@@ -4954,7 +5571,7 @@ function createRunner(deps) {
4954
5571
  limit: z4.number().default(5).describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF")
4955
5572
  }),
4956
5573
  execute: async ({ query, limit }) => {
4957
- return searchKnowledge(sql2, embedModel, agentId, query, limit);
5574
+ return searchKnowledge(sql2, provider, agentId, query, limit);
4958
5575
  }
4959
5576
  };
4960
5577
  }
@@ -4965,8 +5582,7 @@ function createRunner(deps) {
4965
5582
  }
4966
5583
  const system = agent2.system_prompt || void 0;
4967
5584
  if (params.stream) {
4968
- const result = streamText2({
4969
- model,
5585
+ const result = provider.streamText({
4970
5586
  system,
4971
5587
  messages: messages2,
4972
5588
  tools: Object.keys(tools).length > 0 ? tools : void 0
@@ -4991,8 +5607,7 @@ function createRunner(deps) {
4991
5607
  });
4992
5608
  return { stream: sseStream };
4993
5609
  } else {
4994
- const result = await generateText3({
4995
- model,
5610
+ const result = await provider.generateText({
4996
5611
  system,
4997
5612
  messages: messages2,
4998
5613
  tools: Object.keys(tools).length > 0 ? tools : void 0
@@ -5011,20 +5626,20 @@ function createRunner(deps) {
5011
5626
  }
5012
5627
  }
5013
5628
  async function addKnowledge(agentId, title, content) {
5014
- const embedModel = getEmbeddingModel();
5015
5629
  const chunks = chunkContent(content);
5016
5630
  const [first] = chunks;
5017
- const { embedding } = await embed2({ model: embedModel, value: first });
5631
+ const embedding = await provider.embed(first);
5018
5632
  const vec = `[${embedding.join(",")}]`;
5019
5633
  const [doc] = await sql2.unsafe(
5020
5634
  `INSERT INTO "_knowledge_documents" ("agent_id", "title", "content", "embedding") VALUES ($1, $2, $3, $4::vector) RETURNING *`,
5021
5635
  [agentId, title, first, vec]
5022
5636
  );
5023
5637
  for (let i = 1; i < chunks.length; i++) {
5024
- const { embedding: emb } = await embed2({ model: embedModel, value: chunks[i] });
5638
+ const emb = await provider.embed(chunks[i]);
5639
+ const vec2 = `[${emb.join(",")}]`;
5025
5640
  await sql2.unsafe(
5026
5641
  `INSERT INTO "_knowledge_documents" ("agent_id", "title", "content", "embedding") VALUES ($1, $2, $3, $4::vector)`,
5027
- [agentId, `${title} (${i + 1})`, chunks[i], `[${emb.join(",")}]`]
5642
+ [agentId, `${title} (${i + 1})`, chunks[i], vec2]
5028
5643
  );
5029
5644
  }
5030
5645
  return doc;
@@ -5033,31 +5648,11 @@ function createRunner(deps) {
5033
5648
  }
5034
5649
 
5035
5650
  // agent/client.ts
5036
- function createModelsFromEnv() {
5037
- const baseURL = process.env.OPENAI_BASE_URL || "http://localhost:11434/v1";
5038
- const apiKey = process.env.OPENAI_API_KEY || "ollama";
5039
- const modelName = process.env.OPENAI_MODEL || "qwen3:0.6b";
5040
- const embedModelName = process.env.OPENAI_EMBEDDING_MODEL || "qwen3-embedding:0.6b";
5041
- const provider = createOpenAI2({ baseURL, apiKey });
5042
- return {
5043
- model: provider(modelName),
5044
- embeddingModel: provider.embedding(embedModelName),
5045
- dimension: parseInt(process.env.EMBEDDING_DIMENSION || "1024", 10)
5046
- };
5047
- }
5048
5651
  function agent(options) {
5049
5652
  const pg = options.pg;
5050
5653
  const sql2 = pg.sql;
5051
- const model = options.model;
5052
- const embeddingModel = options.embeddingModel;
5053
- const dimension = options.embeddingDimension ?? 1024;
5054
- const defaultModels = !model || !embeddingModel ? createModelsFromEnv() : null;
5055
- function getModel() {
5056
- return model ?? defaultModels.model;
5057
- }
5058
- function getEmbeddingModel() {
5059
- return embeddingModel ?? defaultModels.embeddingModel;
5060
- }
5654
+ const resolvedProvider = options.provider ?? aiProvider();
5655
+ const dimension = options.embeddingDimension ?? resolvedProvider.dimension;
5061
5656
  const agentsTable = pg.table("_agents", {
5062
5657
  id: serial("id").primaryKey(),
5063
5658
  tenant_id: text("tenant_id"),
@@ -5094,7 +5689,7 @@ function agent(options) {
5094
5689
  trace_id: text("trace_id"),
5095
5690
  created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
5096
5691
  });
5097
- const runner = createRunner({ sql: sql2, agents: agentsTable, runs: runsTable, knowledge: knowledgeTable, getModel, getEmbeddingModel, userTools: options.tools });
5692
+ const runner = createRunner({ sql: sql2, agents: agentsTable, runs: runsTable, knowledge: knowledgeTable, provider: resolvedProvider, userTools: options.tools });
5098
5693
  const base = new PgModule(pg);
5099
5694
  const r = buildRouter2({ agents: agentsTable, runs: runsTable, knowledge: knowledgeTable, runner });
5100
5695
  const mod = r;
@@ -5635,7 +6230,7 @@ function createGateway(config, getPort) {
5635
6230
  }
5636
6231
 
5637
6232
  // deploy/manager.ts
5638
- import crypto5 from "node:crypto";
6233
+ import crypto6 from "node:crypto";
5639
6234
 
5640
6235
  // deploy/process.ts
5641
6236
  import { fork } from "node:child_process";
@@ -5694,7 +6289,7 @@ function createManager(config, apps, manager) {
5694
6289
  const token = header.replace("Bearer ", "");
5695
6290
  const tokenBuf = Buffer.from(token);
5696
6291
  const secretBuf = Buffer.from(config.deployToken);
5697
- if (tokenBuf.length !== secretBuf.length || !crypto5.timingSafeEqual(tokenBuf, secretBuf)) {
6292
+ if (tokenBuf.length !== secretBuf.length || !crypto6.timingSafeEqual(tokenBuf, secretBuf)) {
5698
6293
  return Response.json({ error: "Unauthorized" }, { status: 401 });
5699
6294
  }
5700
6295
  return next(req, ctx);
@@ -5990,7 +6585,7 @@ import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
5990
6585
  // ssr.ts
5991
6586
  import { createElement as createElement3 } from "react";
5992
6587
  import { createHash as createHash3 } from "node:crypto";
5993
- import { existsSync as existsSync4, readdirSync } from "node:fs";
6588
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "node:fs";
5994
6589
  import { readdir, stat } from "node:fs/promises";
5995
6590
  import { dirname as dirname3, join as join5, resolve as resolve6, relative as relative2 } from "node:path";
5996
6591
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
@@ -6107,6 +6702,7 @@ function compile(path2) {
6107
6702
  return isDev() ? compileTsxDev(path2) : compileTsx(path2);
6108
6703
  }
6109
6704
  var vendorBundle = null;
6705
+ var vendorHash = "";
6110
6706
  async function compileVendorBundle() {
6111
6707
  if (vendorBundle) return vendorBundle;
6112
6708
  if (!_userRequire) _userRequire = createRequire(join2(process.cwd(), "package.json"));
@@ -6121,8 +6717,18 @@ async function compileVendorBundle() {
6121
6717
  const keys = Object.keys(mod).filter((k) => !k.startsWith("_") && k !== "default");
6122
6718
  modules[request] = keys;
6123
6719
  }
6124
- const wfwMod = _userRequire("weifuwu/react");
6125
- const wfwKeys = Object.keys(wfwMod).filter((k) => !k.startsWith("_") && k !== "default");
6720
+ const reactTsPath = resolve3(import.meta.dirname ?? __dirname, "react.ts");
6721
+ const reactSrc = readFileSync2(reactTsPath, "utf-8");
6722
+ const wfwKeys = [];
6723
+ for (const line of reactSrc.split("\n")) {
6724
+ const m = line.match(/^export\s+\{[^}]+\}\s*from/);
6725
+ if (m) {
6726
+ const names = line.slice(line.indexOf("{") + 1, line.indexOf("}")).split(",").map((s) => s.trim()).filter(Boolean);
6727
+ for (const n of names) {
6728
+ if (!n.startsWith("type ") && !wfwKeys.includes(n)) wfwKeys.push(n);
6729
+ }
6730
+ }
6731
+ }
6126
6732
  const used = /* @__PURE__ */ new Set();
6127
6733
  const stmts = [""];
6128
6734
  for (const [request, keys] of Object.entries(modules)) {
@@ -6130,7 +6736,7 @@ async function compileVendorBundle() {
6130
6736
  if (unique.length > 0) stmts.push(`export { ${unique.join(", ")} } from ${JSON.stringify(request)};`);
6131
6737
  }
6132
6738
  const uidWfw = wfwKeys.filter((k) => !used.has(k) && used.add(k));
6133
- if (uidWfw.length > 0) stmts.push(`export { ${uidWfw.join(", ")} } from 'weifuwu/react';`);
6739
+ if (uidWfw.length > 0) stmts.push(`export { ${uidWfw.join(", ")} } from ${JSON.stringify(reactTsPath)};`);
6134
6740
  const result = await esbuild.build({
6135
6741
  stdin: { contents: stmts.join("\n"), resolveDir: process.cwd() },
6136
6742
  format: "esm",
@@ -6138,13 +6744,68 @@ async function compileVendorBundle() {
6138
6744
  write: false
6139
6745
  });
6140
6746
  vendorBundle = new TextDecoder().decode(result.outputFiles[0].contents);
6747
+ const hashBytes = new TextEncoder().encode(vendorBundle);
6748
+ const hashBuffer = await crypto.subtle.digest("SHA-1", hashBytes);
6749
+ vendorHash = Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 8);
6141
6750
  return vendorBundle;
6142
6751
  }
6752
+ async function compileBrowser(path2, outDir) {
6753
+ const absPath = resolve3(path2);
6754
+ const h = id(absPath);
6755
+ outDir = outDir ?? resolve3(OUT_DIR);
6756
+ const outPath = join2(outDir, h + ".js");
6757
+ if (!isDev() && existsSync(outPath)) return h;
6758
+ mkdirSync(outDir, { recursive: true });
6759
+ const wfwDir = resolve3(import.meta.dirname ?? __dirname);
6760
+ const plugin = {
6761
+ name: "wfw-external",
6762
+ setup(build2) {
6763
+ build2.onResolve({ filter: /./ }, (args) => {
6764
+ if (args.kind === "entry-point") return;
6765
+ const abs = args.path.startsWith(".") ? join2(args.resolveDir, args.path) : args.path;
6766
+ if (abs.startsWith(wfwDir) && !abs.includes("node_modules")) {
6767
+ const rel = abs.slice(wfwDir.length + 1);
6768
+ if (rel.includes("/")) return;
6769
+ return { path: "weifuwu/react", external: true };
6770
+ }
6771
+ });
6772
+ }
6773
+ };
6774
+ await esbuild.build({
6775
+ entryPoints: { [h]: absPath },
6776
+ outdir: outDir,
6777
+ format: "esm",
6778
+ platform: "browser",
6779
+ jsx: "automatic",
6780
+ jsxImportSource: "react",
6781
+ bundle: true,
6782
+ external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu", "weifuwu/react"],
6783
+ plugins: [plugin],
6784
+ write: true,
6785
+ allowOverwrite: true
6786
+ });
6787
+ return h;
6788
+ }
6143
6789
  async function compileHotComponent(path2) {
6144
6790
  const absPath = resolve3(path2);
6145
6791
  const h = id(absPath);
6146
6792
  const stdin = `import C from ${JSON.stringify(absPath)};
6147
6793
  (window.__WFW_REFRESH||function(){})(C)`;
6794
+ const wfwDir = resolve3(import.meta.dirname ?? __dirname);
6795
+ const plugin = {
6796
+ name: "wfw-external",
6797
+ setup(build2) {
6798
+ build2.onResolve({ filter: /./ }, (args) => {
6799
+ if (args.kind === "entry-point") return;
6800
+ const abs = args.path.startsWith(".") ? join2(args.resolveDir, args.path) : args.path;
6801
+ if (abs.startsWith(wfwDir) && !abs.includes("node_modules")) {
6802
+ const rel = abs.slice(wfwDir.length + 1);
6803
+ if (rel.includes("/")) return;
6804
+ return { path: "weifuwu/react", external: true };
6805
+ }
6806
+ });
6807
+ }
6808
+ };
6148
6809
  const result = await esbuild.build({
6149
6810
  stdin: { contents: stdin, loader: "tsx", resolveDir: dirname(absPath) },
6150
6811
  format: "esm",
@@ -6152,7 +6813,8 @@ async function compileHotComponent(path2) {
6152
6813
  jsx: "automatic",
6153
6814
  jsxImportSource: "react",
6154
6815
  bundle: true,
6155
- external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu/react"],
6816
+ external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu", "weifuwu/react"],
6817
+ plugins: [plugin],
6156
6818
  write: false
6157
6819
  });
6158
6820
  let code = new TextDecoder().decode(result.outputFiles[0].contents);
@@ -6177,11 +6839,10 @@ function getPublicEnv() {
6177
6839
  return _publicEnv;
6178
6840
  }
6179
6841
  function buildHeadPayload(opts) {
6180
- const { ctx, base, compiledTailwindCss, isDev: isDev3 } = opts;
6842
+ const { ctx, base, tailwind } = opts;
6181
6843
  let result = "";
6182
- if (isDev3) {
6183
- const vUrl = `${base}/__wfw/v/bundle`;
6184
- result += `<script type="importmap">{
6844
+ const vUrl = `${base}/__wfw/v/bundle?h=${vendorHash}`;
6845
+ result += `<script type="importmap">{
6185
6846
  "imports": {
6186
6847
  "react": "${vUrl}",
6187
6848
  "react-dom": "${vUrl}",
@@ -6191,22 +6852,12 @@ function buildHeadPayload(opts) {
6191
6852
  }
6192
6853
  }</script>
6193
6854
  `;
6194
- }
6195
- if (ctx.prefs?.theme) {
6855
+ if (ctx.theme?.value) {
6196
6856
  result += `<script>!function(){var t=(document.cookie.match(/(?:^|;\\s*)theme=([^;]+)/)||[])[1]||'system';if(t==='system'){t=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'}document.documentElement.setAttribute('data-theme',t)}()</script>
6197
6857
  `;
6198
6858
  }
6199
- if (compiledTailwindCss) {
6200
- const cssUrl = ctx.tailwindCssUrl;
6201
- if (cssUrl) result += `<link rel="stylesheet" href="${cssUrl}" />
6202
- `;
6203
- }
6204
- const localeData = ctx.parsed?.__localeData ?? globalThis.__LOCALE_DATA__;
6205
- if (localeData && Object.keys(localeData).length > 0) {
6206
- if (!_localeDataCache || _localeDataCache.data !== localeData) {
6207
- _localeDataCache = { data: localeData, json: JSON.stringify(localeData) };
6208
- }
6209
- result += `<script>window.__LOCALE_DATA__=${_localeDataCache.json}</script>
6859
+ if (tailwind?.css) {
6860
+ result += `<link rel="stylesheet" href="${tailwind.url}" />
6210
6861
  `;
6211
6862
  }
6212
6863
  const loaderData = opts.loaderData || {};
@@ -6214,7 +6865,9 @@ function buildHeadPayload(opts) {
6214
6865
  params: ctx.params,
6215
6866
  query: ctx.query,
6216
6867
  parsed: ctx.parsed,
6217
- prefs: ctx.prefs,
6868
+ theme: ctx.theme,
6869
+ i18n: ctx.i18n,
6870
+ flash: ctx.flash,
6218
6871
  loaderData
6219
6872
  };
6220
6873
  if (ctx.user && typeof ctx.user === "object") {
@@ -6234,72 +6887,97 @@ function buildHeadPayload(opts) {
6234
6887
  `;
6235
6888
  return result;
6236
6889
  }
6237
- function buildBodyScripts(opts) {
6890
+ function buildBodyScripts(opts, hydrationScript) {
6238
6891
  const parts = [];
6239
- if (opts.loaderData && Object.keys(opts.loaderData).length > 0) {
6240
- parts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(opts.loaderData)}</script>`);
6241
- }
6242
- if (opts.bundle) {
6243
- parts.push(`<script type="module" src="${opts.base}${opts.bundle.url}"></script>`);
6244
- }
6892
+ if (hydrationScript) parts.push(hydrationScript);
6245
6893
  return parts.join("\n");
6246
6894
  }
6247
- var _localeDataCache = null;
6248
- function streamResponse(reactStream, opts) {
6895
+ function streamResponse(reactStream, opts, hydrationScript) {
6249
6896
  const decoder = new TextDecoder2();
6250
6897
  const encoder2 = new TextEncoder2();
6251
- const headPayload = buildHeadPayload(opts);
6252
- let buffer = "";
6253
- let headFlushed = false;
6254
- let extractedHead = "";
6255
6898
  const output = new ReadableStream({
6256
6899
  async start(controller) {
6257
6900
  try {
6258
6901
  const reader = reactStream.getReader();
6259
- async function push(chunk) {
6260
- buffer += decoder.decode(chunk, { stream: true });
6261
- if (!extractedHead) {
6262
- const m = buffer.match(/<template id="__wfw_head">([\s\S]*?)<\/template>/);
6263
- if (m) {
6264
- extractedHead = m[1];
6265
- buffer = buffer.replace(m[0], "");
6266
- }
6267
- }
6268
- if (!headFlushed) {
6269
- const idx = buffer.indexOf("</head>");
6270
- if (idx !== -1) {
6271
- const before = buffer.slice(0, idx);
6272
- let injection = "";
6273
- if (extractedHead) injection += "\n" + extractedHead;
6274
- injection += headPayload;
6275
- controller.enqueue(encoder2.encode(before + injection));
6276
- buffer = buffer.slice(idx);
6277
- headFlushed = true;
6278
- }
6279
- return;
6280
- }
6281
- controller.enqueue(encoder2.encode(buffer));
6282
- buffer = "";
6283
- }
6902
+ let html = "";
6284
6903
  while (true) {
6285
6904
  const { done, value } = await reader.read();
6286
6905
  if (done) break;
6287
- await push(value);
6906
+ html += decoder.decode(value, { stream: true });
6907
+ }
6908
+ html += decoder.decode();
6909
+ const headTmpl = html.match(/<template id="__wfw_head">([\s\S]*?)<\/template>/);
6910
+ if (headTmpl) {
6911
+ const extractedHead = headTmpl[1];
6912
+ html = html.replace(headTmpl[0], "");
6913
+ const headIdx2 = html.indexOf("</head>");
6914
+ if (headIdx2 !== -1) {
6915
+ html = html.slice(0, headIdx2) + "\n" + extractedHead + html.slice(headIdx2);
6916
+ }
6917
+ }
6918
+ const headPayload = buildHeadPayload(opts);
6919
+ const headIdx = html.indexOf("</head>");
6920
+ if (headIdx !== -1) {
6921
+ html = html.slice(0, headIdx) + headPayload + html.slice(headIdx);
6288
6922
  }
6289
- buffer = buffer.replace(/<template id="__wfw_head">[\s\S]*?<\/template>/g, "");
6290
- if (buffer) controller.enqueue(encoder2.encode(buffer));
6291
- const body = buildBodyScripts(opts);
6292
- if (body) controller.enqueue(encoder2.encode("\n" + body));
6923
+ let bodyScripts = "";
6924
+ const built = buildBodyScripts(opts, hydrationScript);
6925
+ if (built) bodyScripts += built;
6293
6926
  if (opts.isDev) {
6294
6927
  const wsUrl = `${opts.base}/__weifuwu/livereload`;
6295
6928
  const hbUrl = `${opts.base}/__wfw/h/`;
6296
- controller.enqueue(encoder2.encode(
6297
- `
6298
- <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${wsUrl}');var t=0;ws.onmessage=function(e){try{var m=JSON.parse(e.data);if(m.type==='component'){if(m.entry&&m.entry!==window.__WFW_ENTRY)return;import('${hbUrl}'+m.hash+'?'+Date.now()).catch(function(){location.reload()});if(m.css){var s=document.querySelector('style[data-lr]')||function(){var x=document.createElement('style');x.setAttribute('data-lr','');document.head.appendChild(x);return x}();s.textContent=m.css}return}if(m.type==='css'){var s=document.querySelector('style[data-lr]')||function(){var x=document.createElement('style');x.setAttribute('data-lr','');document.head.appendChild(x);return x}();s.textContent=m.css;return}}catch(_){}if(e.data==='reload'&&Date.now()-t>1e3){t=Date.now();location.reload()}};ws.onclose=function(){if(Date.now()-t>1e3){t=Date.now();setTimeout(function(){location.reload()},500)}}})()</script>`
6299
- ));
6929
+ bodyScripts += `
6930
+ <script>
6931
+ (function(){
6932
+ var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${wsUrl}');
6933
+ var t=0;
6934
+ ws.onmessage=function(e){
6935
+ try{
6936
+ var m=JSON.parse(e.data);
6937
+ if(m.type==='component'){
6938
+ import('${hbUrl}'+m.hash+'?t='+Date.now()).catch(function(){location.reload()});
6939
+ if(m.css){
6940
+ var s=document.querySelector('style[data-lr]')||function(){
6941
+ var x=document.createElement('style');
6942
+ x.setAttribute('data-lr','');
6943
+ document.head.appendChild(x);
6944
+ return x
6945
+ }();
6946
+ s.textContent=m.css
6947
+ }
6948
+ return
6949
+ }
6950
+ if(m.type==='css'){
6951
+ var s=document.querySelector('style[data-lr]')||function(){
6952
+ var x=document.createElement('style');
6953
+ x.setAttribute('data-lr','');
6954
+ document.head.appendChild(x);
6955
+ return x
6956
+ }();
6957
+ s.textContent=m.css
6958
+ return
6959
+ }
6960
+ }catch(_){}
6961
+ if(e.data==='reload'&&Date.now()-t>1e3){t=Date.now();location.reload()}
6962
+ };
6963
+ ws.onclose=function(){
6964
+ if(Date.now()-t>1e3){
6965
+ t=Date.now();
6966
+ setTimeout(function(){location.reload()},500)
6967
+ }
6968
+ };
6969
+ })();
6970
+ </script>`;
6971
+ }
6972
+ if (bodyScripts) {
6973
+ const bodyIdx = html.lastIndexOf("</body>");
6974
+ if (bodyIdx !== -1) {
6975
+ html = html.slice(0, bodyIdx) + bodyScripts + html.slice(bodyIdx);
6976
+ }
6300
6977
  }
6978
+ controller.enqueue(encoder2.encode(html));
6301
6979
  } catch {
6302
- const fallback = `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>500</title></head><body><h1>500 - Internal Server Error</h1></body></html>`;
6980
+ const fallback = '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>500</title></head><body><h1>500 - Internal Server Error</h1></body></html>';
6303
6981
  controller.enqueue(encoder2.encode(fallback));
6304
6982
  } finally {
6305
6983
  controller.close();
@@ -6329,9 +7007,9 @@ function tailwindContext(dir) {
6329
7007
  await compileTailwindCss(cssPath, cssDir);
6330
7008
  }
6331
7009
  const entry = cssCache.get(cssPath);
6332
- ctx.compiledTailwindCss = entry.css;
6333
7010
  const base = (ctx.mountPath || "").replace(/\/$/, "");
6334
- ctx.tailwindCssUrl = base ? `${base}/__wfw/style/${entry.hash}.css` : `/__wfw/style/${entry.hash}.css`;
7011
+ const url = base ? `${base}/__wfw/style/${entry.hash}.css` : `/__wfw/style/${entry.hash}.css`;
7012
+ ctx.tailwind = { css: entry.css, url };
6335
7013
  return next(req, ctx);
6336
7014
  };
6337
7015
  }
@@ -6425,6 +7103,8 @@ function liveWs() {
6425
7103
  }
6426
7104
  function liveRouter(dir) {
6427
7105
  const r = new Router();
7106
+ compileVendorBundle().catch(() => {
7107
+ });
6428
7108
  r.get("/__wfw/v/bundle", async () => {
6429
7109
  const code = await compileVendorBundle();
6430
7110
  return new Response(code, {
@@ -6472,7 +7152,6 @@ function liveWatcher(dir) {
6472
7152
  return broadcastReload();
6473
7153
  }
6474
7154
  clearCompileCache();
6475
- markClientBundleDirty();
6476
7155
  const targets = existsSync3(entryPath) ? [entryPath] : findEntries(resolve5(filePath));
6477
7156
  if (targets.length === 0) return broadcastReload();
6478
7157
  try {
@@ -6578,7 +7257,7 @@ function errorBoundary(errorPath) {
6578
7257
  ctx,
6579
7258
  base,
6580
7259
  isDev: isDev(),
6581
- compiledTailwindCss: ctx.compiledTailwindCss,
7260
+ tailwind: ctx.tailwind,
6582
7261
  status: 500
6583
7262
  });
6584
7263
  }
@@ -6589,25 +7268,6 @@ function errorBoundary(errorPath) {
6589
7268
  var isDev2 = isDev();
6590
7269
  var als2 = new AsyncLocalStorage2();
6591
7270
  __registerAls(() => als2.getStore());
6592
- var bundleCache = /* @__PURE__ */ new Map();
6593
- var _bundleDirty = false;
6594
- function markClientBundleDirty() {
6595
- _bundleDirty = true;
6596
- }
6597
- function getBundle(key) {
6598
- if (_bundleDirty) {
6599
- bundleCache.clear();
6600
- _bundleDirty = false;
6601
- }
6602
- return bundleCache.get(key);
6603
- }
6604
- function setBundle(key, buf) {
6605
- if (_bundleDirty) {
6606
- bundleCache.clear();
6607
- _bundleDirty = false;
6608
- }
6609
- bundleCache.set(key, buf);
6610
- }
6611
7271
  function hashId(s) {
6612
7272
  return createHash3("md5").update(s).digest("hex").slice(0, 8);
6613
7273
  }
@@ -6722,50 +7382,40 @@ async function resolveRoute(ssrDir, segments, routeCache) {
6722
7382
  routeCache.set(cacheKey, result);
6723
7383
  return result;
6724
7384
  }
6725
- async function buildClientBundle(entryPath, layoutPaths) {
6726
- try {
6727
- const absEntry = resolve6(entryPath);
6728
- const absLayouts = layoutPaths.map((p) => resolve6(p));
6729
- const layoutImports = absLayouts.map((p) => `import${JSON.stringify(p)};`).join("");
6730
- const _sc = `(function(){var k='__WEIFUWU_CTX_STORE';var s=typeof globalThis!='undefined'&&globalThis[k];if(!s)return function(){};return function(v){s._ctx={...s._ctx,...v};s._snapshot={params:s._ctx.params,query:s._ctx.query,user:s._ctx.user,parsed:s._ctx.parsed,prefs:s._ctx.prefs,env:s._ctx.env};s._listeners.forEach(function(fn){fn()})}})()`;
6731
- const code = [
6732
- layoutImports,
6733
- `${isDev2 ? "import{createRoot}from'react-dom/client';" : "import{hydrateRoot}from'react-dom/client';"}`,
6734
- `import{createElement}from'react';`,
6735
- `import{TsxContext}from'weifuwu/react';`,
6736
- `import P from${JSON.stringify(absEntry)};`,
6737
- `var setCtx=${_sc};`,
6738
- `const c=document.getElementById('__weifuwu_root');`,
6739
- `if(window.__WEIFUWU_PROPS)setCtx({loaderData:window.__WEIFUWU_PROPS});`,
6740
- isDev2 ? `const _W=function(props){return(_W._fn||P)(props)};_W._fn=P;const _P=function(props){return createElement(_W,props)};` : "",
6741
- isDev2 ? `window.__WFW_ENTRY=${JSON.stringify(hashId(absEntry))};window.__WFW_REFRESH=function(n){_W._fn=n;window.__WFW_ROOT.render(createElement(App))};` : "",
6742
- `function App(){`,
6743
- `const ctx=window.__WEIFUWU_CTX||{};`,
6744
- `return createElement(TsxContext.Provider,{value:ctx},`,
6745
- isDev2 ? `createElement(_P,null))` : `createElement(P,null))`,
6746
- `}`,
6747
- isDev2 ? `window.__WFW_ROOT=createRoot(c);window.__WFW_ROOT.render(createElement(App));` : `hydrateRoot(c,createElement(App));`
6748
- ].filter(Boolean).join("");
6749
- const { default: esbuild2 } = await import("esbuild");
6750
- const result = await esbuild2.build({
6751
- stdin: { contents: code, loader: "tsx", resolveDir: dirname3(absEntry) },
6752
- bundle: true,
6753
- format: "esm",
6754
- jsx: "automatic",
6755
- jsxImportSource: "react",
6756
- banner: { js: "self.process={env:{}};" },
6757
- loader: { ".node": "empty" },
6758
- external: isDev2 ? ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu", "weifuwu/react"] : void 0,
6759
- write: false,
6760
- minify: !isDev2
6761
- });
6762
- return result.outputFiles[0].contents;
6763
- } catch (err) {
6764
- console.error("hydration bundle failed:", err);
6765
- return null;
6766
- }
7385
+ function buildHydrationScript(entryId, ctxJson, base) {
7386
+ const ssrPrefix = `${base}/__ssr`;
7387
+ return `
7388
+ <script type="module">
7389
+ import { setCtx, TsxContext } from 'weifuwu/react';
7390
+ import { createElement } from 'react';
7391
+ import { hydrateRoot, createRoot } from 'react-dom/client';
7392
+
7393
+ const _ctx = ${ctxJson};
7394
+ setCtx(_ctx);
7395
+
7396
+ const _root = document.getElementById('__weifuwu_root');
7397
+
7398
+ async function init() {
7399
+ const { default: Page } = await import('${ssrPrefix}/${entryId}.js');
7400
+ const app = createElement(TsxContext.Provider, { value: _ctx },
7401
+ createElement(Page));
7402
+ ${isDev2 ? `
7403
+ const reactRoot = createRoot(_root);
7404
+ reactRoot.render(app);
7405
+ window.__WFW_REFRESH = async (NewComponent) => {
7406
+ const store = globalThis.__WEIFUWU_CTX_STORE?._ctx || _ctx;
7407
+ reactRoot.render(createElement(TsxContext.Provider, { value: store },
7408
+ createElement(NewComponent)));
7409
+ };
7410
+ ` : `
7411
+ hydrateRoot(_root, app);
7412
+ `}
6767
7413
  }
6768
- function renderPage(pageFile) {
7414
+
7415
+ init();
7416
+ </script>`;
7417
+ }
7418
+ function renderPage(pageFile, outDir) {
6769
7419
  const absPath = resolve6(pageFile);
6770
7420
  const entryId = hashId(absPath);
6771
7421
  ssrEntries.set(entryId, { path: absPath });
@@ -6791,15 +7441,15 @@ function renderPage(pageFile) {
6791
7441
  query: ctx.query,
6792
7442
  user: ctx.user ?? {},
6793
7443
  parsed: ctx.parsed ?? {},
6794
- prefs: ctx.prefs ?? {},
7444
+ theme: ctx.theme,
7445
+ i18n: ctx.i18n,
7446
+ flash: ctx.flash,
6795
7447
  loaderData,
6796
7448
  env: ctx.env ?? {}
6797
7449
  };
6798
7450
  return als2.run(ctxValue, async () => {
6799
7451
  setCtx(ctxValue);
6800
- if (ctxValue.parsed?.__localeData) {
6801
- globalThis.__LOCALE_DATA__ = ctxValue.parsed.__localeData;
6802
- }
7452
+ await compileBrowser(absPath, outDir);
6803
7453
  let element = createElement3(
6804
7454
  "div",
6805
7455
  { id: "__weifuwu_root" },
@@ -6810,24 +7460,15 @@ function renderPage(pageFile) {
6810
7460
  )
6811
7461
  );
6812
7462
  element = buildHtmlShell("weifuwu", element, layoutComponents);
6813
- let bundle = null;
6814
- if (!getBundle(bundleKey)) {
6815
- const buf = await buildClientBundle(absPath, layoutPaths);
6816
- if (buf) setBundle(bundleKey, buf);
6817
- }
6818
- if (getBundle(bundleKey)) {
6819
- bundle = { url: bundleKey };
6820
- }
6821
7463
  const { renderToReadableStream } = await import("react-dom/server");
6822
7464
  const stream = await renderToReadableStream(element);
6823
7465
  return streamResponse(stream, {
6824
7466
  ctx,
6825
7467
  base,
6826
7468
  isDev: isDev2,
6827
- bundle,
6828
7469
  loaderData,
6829
- compiledTailwindCss: ctx.compiledTailwindCss
6830
- });
7470
+ tailwind: ctx.tailwind
7471
+ }, buildHydrationScript(entryId, JSON.stringify(ctxValue), base));
6831
7472
  });
6832
7473
  };
6833
7474
  }
@@ -6873,11 +7514,23 @@ function discoverRoutes(dir) {
6873
7514
  function ssr(opts) {
6874
7515
  const r = new Router();
6875
7516
  const dir = resolve6(opts.dir);
7517
+ const outDir = resolve6(OUT_DIR);
6876
7518
  const routeCache = /* @__PURE__ */ new Map();
6877
- r.get("/__ssr/:path", (req, ctx) => {
6878
- const buf = getBundle("/__ssr/" + ctx.params.path);
6879
- if (!buf) return new Response("", { status: 404 });
6880
- return new Response(buf, {
7519
+ compileVendorBundle().catch(() => {
7520
+ });
7521
+ r.get("/__ssr/:file", (req, ctx) => {
7522
+ const filePath = join5(outDir, ctx.params.file);
7523
+ if (!filePath.startsWith(outDir) || !existsSync4(filePath)) {
7524
+ return new Response("Not Found", { status: 404 });
7525
+ }
7526
+ const content = readFileSync4(filePath, "utf-8");
7527
+ return new Response(content, {
7528
+ headers: { "content-type": "application/javascript; charset=utf-8" }
7529
+ });
7530
+ });
7531
+ r.get("/__wfw/v/bundle", async () => {
7532
+ const code = await compileVendorBundle();
7533
+ return new Response(code, {
6881
7534
  headers: { "content-type": "application/javascript; charset=utf-8" }
6882
7535
  });
6883
7536
  });
@@ -6904,7 +7557,7 @@ function ssr(opts) {
6904
7557
  ...resolved.layoutFiles.map((f) => layout(f)),
6905
7558
  tailwindContext(dir)
6906
7559
  ];
6907
- const handler = (req2, ctx2) => renderPage(resolved.pageFile)(req2, ctx2);
7560
+ const handler = (req2, ctx2) => renderPage(resolved.pageFile, outDir)(req2, ctx2);
6908
7561
  return runChain(mws, handler, req, ctx);
6909
7562
  });
6910
7563
  const mod = r;
@@ -6994,13 +7647,13 @@ async function addToolMessages(sql2, sessionId, toolCalls, toolResults) {
6994
7647
  }
6995
7648
 
6996
7649
  // opencode/run.ts
6997
- import { streamText as streamText3, stepCountIs } from "ai";
7650
+ import { streamText as streamText2, stepCountIs } from "ai";
6998
7651
  async function* executeGenerator(opts) {
6999
7652
  const { sessionId, input, model, tools, systemPrompt, messages: messages2, sql: sql2, abortSignal } = opts;
7000
7653
  const lastStepToolCalls = [];
7001
7654
  let currentAssistantText = "";
7002
7655
  let currentUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
7003
- const result = streamText3({
7656
+ const result = streamText2({
7004
7657
  model,
7005
7658
  system: systemPrompt,
7006
7659
  messages: [
@@ -7167,7 +7820,7 @@ function createBashTool(ctx) {
7167
7820
  // opencode/tools/read.ts
7168
7821
  import { tool as tool4 } from "ai";
7169
7822
  import { z as z6 } from "zod";
7170
- import { readFileSync as readFileSync4 } from "node:fs";
7823
+ import { readFileSync as readFileSync5 } from "node:fs";
7171
7824
  import { resolve as resolve7 } from "node:path";
7172
7825
  function createReadTool(ctx) {
7173
7826
  return tool4({
@@ -7179,10 +7832,10 @@ function createReadTool(ctx) {
7179
7832
  }),
7180
7833
  execute: async ({ path: path2, offset, limit }) => {
7181
7834
  const resolved = resolve7(ctx.workspace, path2);
7182
- if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
7835
+ if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
7183
7836
  return { error: "Path not allowed", content: null, totalLines: 0 };
7184
7837
  }
7185
- const content = readFileSync4(resolved, "utf-8");
7838
+ const content = readFileSync5(resolved, "utf-8");
7186
7839
  const lines = content.split("\n");
7187
7840
  const totalLines = lines.length;
7188
7841
  if (offset !== void 0) {
@@ -7220,7 +7873,7 @@ function createWriteTool(ctx) {
7220
7873
  }),
7221
7874
  execute: async ({ path: path2, content }) => {
7222
7875
  const resolved = resolve8(ctx.workspace, path2);
7223
- if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
7876
+ if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
7224
7877
  return { error: "Path not allowed" };
7225
7878
  }
7226
7879
  mkdirSync3(dirname4(resolved), { recursive: true });
@@ -7233,7 +7886,7 @@ function createWriteTool(ctx) {
7233
7886
  // opencode/tools/edit.ts
7234
7887
  import { tool as tool6 } from "ai";
7235
7888
  import { z as z8 } from "zod";
7236
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
7889
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
7237
7890
  import { resolve as resolve9 } from "node:path";
7238
7891
  function createEditTool(ctx) {
7239
7892
  return tool6({
@@ -7246,10 +7899,10 @@ function createEditTool(ctx) {
7246
7899
  }),
7247
7900
  execute: async ({ path: path2, oldString, newString, replaceAll }) => {
7248
7901
  const resolved = resolve9(ctx.workspace, path2);
7249
- if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
7902
+ if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
7250
7903
  return { error: "Path not allowed" };
7251
7904
  }
7252
- const content = readFileSync5(resolved, "utf-8");
7905
+ const content = readFileSync6(resolved, "utf-8");
7253
7906
  if (replaceAll) {
7254
7907
  if (!content.includes(oldString)) {
7255
7908
  return { error: "oldString not found in file", replaced: 0 };
@@ -7434,7 +8087,7 @@ ${availableList}
7434
8087
  name: z13.string().describe("The name of the skill to load")
7435
8088
  }),
7436
8089
  execute: async ({ name }) => {
7437
- if (!isSkillAllowed(name, ctx.permissions)) {
8090
+ if (!isSkillAllowed(name, ctx.permissions.permissions)) {
7438
8091
  return { error: `Skill "${name}" is not permitted` };
7439
8092
  }
7440
8093
  const skill = ctx.skillsRegistry.get(name);
@@ -7455,28 +8108,28 @@ ${availableList}
7455
8108
  // opencode/tools/index.ts
7456
8109
  function createTools(ctx) {
7457
8110
  const tools = {};
7458
- if (isToolEnabled("bash", ctx.permissions)) {
8111
+ if (isToolEnabled("bash", ctx.permissions.permissions)) {
7459
8112
  tools.bash = createBashTool(ctx);
7460
8113
  }
7461
- if (isToolEnabled("read", ctx.permissions)) {
8114
+ if (isToolEnabled("read", ctx.permissions.permissions)) {
7462
8115
  tools.read = createReadTool(ctx);
7463
8116
  }
7464
- if (isToolEnabled("write", ctx.permissions)) {
8117
+ if (isToolEnabled("write", ctx.permissions.permissions)) {
7465
8118
  tools.write = createWriteTool(ctx);
7466
8119
  }
7467
- if (isToolEnabled("edit", ctx.permissions)) {
8120
+ if (isToolEnabled("edit", ctx.permissions.permissions)) {
7468
8121
  tools.edit = createEditTool(ctx);
7469
8122
  }
7470
- if (isToolEnabled("grep", ctx.permissions)) {
8123
+ if (isToolEnabled("grep", ctx.permissions.permissions)) {
7471
8124
  tools.grep = createGrepTool(ctx);
7472
8125
  }
7473
- if (isToolEnabled("glob", ctx.permissions)) {
8126
+ if (isToolEnabled("glob", ctx.permissions.permissions)) {
7474
8127
  tools.glob = createGlobTool(ctx);
7475
8128
  }
7476
- if (isToolEnabled("web", ctx.permissions)) {
8129
+ if (isToolEnabled("web", ctx.permissions.permissions)) {
7477
8130
  tools.web = createWebTool(ctx);
7478
8131
  }
7479
- if (ctx.skillsRegistry.all.length > 0 && isToolEnabled("skill", ctx.permissions)) {
8132
+ if (ctx.skillsRegistry.all.length > 0 && isToolEnabled("skill", ctx.permissions.permissions)) {
7480
8133
  tools.skill = createSkillTool(ctx);
7481
8134
  }
7482
8135
  tools.question = createQuestionTool(ctx);
@@ -7485,7 +8138,7 @@ function createTools(ctx) {
7485
8138
 
7486
8139
  // opencode/rest.ts
7487
8140
  async function buildRouter4(deps) {
7488
- const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
8141
+ const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions: permissions2, pendingQuestions } = deps;
7489
8142
  const router = new Router();
7490
8143
  router.post("/sessions", async (req, ctx) => {
7491
8144
  const body = await req.json().catch(() => ({}));
@@ -7520,7 +8173,7 @@ async function buildRouter4(deps) {
7520
8173
  if (!content) return new Response("Missing content", { status: 400 });
7521
8174
  const toolCtx = {
7522
8175
  workspace: session2.workspace || workspace,
7523
- permissions,
8176
+ permissions: permissions2,
7524
8177
  pendingQuestions,
7525
8178
  skillsRegistry: deps.skillsRegistry
7526
8179
  };
@@ -7578,7 +8231,7 @@ async function buildRouter4(deps) {
7578
8231
  // opencode/ws.ts
7579
8232
  var clients2 = /* @__PURE__ */ new WeakMap();
7580
8233
  function createWSHandler2(deps) {
7581
- const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
8234
+ const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions: permissions2, pendingQuestions } = deps;
7582
8235
  return {
7583
8236
  open(ws, ctx) {
7584
8237
  const userId = ctx.user?.id ?? 0;
@@ -7628,7 +8281,7 @@ function createWSHandler2(deps) {
7628
8281
  }
7629
8282
  const toolCtx = {
7630
8283
  workspace: session2.workspace || workspace,
7631
- permissions,
8284
+ permissions: permissions2,
7632
8285
  pendingQuestions,
7633
8286
  skillsRegistry
7634
8287
  };
@@ -7790,7 +8443,7 @@ async function opencode(options) {
7790
8443
  const workspace = options.workspace || process.cwd();
7791
8444
  const systemPrompt = options.systemPrompt;
7792
8445
  const manualSkills = options.skills || [];
7793
- const permissions = options.permissions;
8446
+ const permissions2 = options.permissions;
7794
8447
  const modelName = options.model || "deepseek-v4-flash";
7795
8448
  const [discoveredSkills] = await Promise.all([discoverSkills(workspace)]);
7796
8449
  const skillsRegistry = buildSkillRegistry(discoveredSkills, manualSkills);
@@ -7798,7 +8451,7 @@ async function opencode(options) {
7798
8451
  const model = provider.chat(modelName);
7799
8452
  const pendingQuestions = /* @__PURE__ */ new Map();
7800
8453
  const base = new PgModule(pg);
7801
- const r = await buildRouter4({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions, pendingQuestions });
8454
+ const r = await buildRouter4({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions: permissions2, pendingQuestions });
7802
8455
  const mod = r;
7803
8456
  mod.migrate = async () => {
7804
8457
  const sessions2 = pg.table("_opencode_sessions", {
@@ -7831,7 +8484,7 @@ async function opencode(options) {
7831
8484
  await messages2.create();
7832
8485
  await messages2.createIndex(["session_id", "created_at"]);
7833
8486
  };
7834
- mod.wsHandler = () => createWSHandler2({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions, pendingQuestions });
8487
+ mod.wsHandler = () => createWSHandler2({ sql: sql2, model, workspace, systemPrompt, skills: manualSkills, skillsRegistry, permissions: permissions2, pendingQuestions });
7835
8488
  mod.close = () => base.close();
7836
8489
  return mod;
7837
8490
  }
@@ -8101,12 +8754,49 @@ function analytics(options) {
8101
8754
  return mod;
8102
8755
  }
8103
8756
 
8104
- // preferences.ts
8757
+ // theme.ts
8758
+ function makeSetTheme(cookie, location) {
8759
+ return (value, loc) => {
8760
+ const finalLoc = loc ?? location;
8761
+ const c = `${cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
8762
+ return new Response(null, { status: 302, headers: { Location: finalLoc, "Set-Cookie": c } });
8763
+ };
8764
+ }
8765
+ function theme(options) {
8766
+ const opts = { default: "system", cookie: "theme", ...options };
8767
+ return async (req, ctx, next) => {
8768
+ const url = new URL(req.url);
8769
+ const match = url.pathname.match(/^\/__theme\/([\w-]+)$/);
8770
+ if (match && req.method === "GET") {
8771
+ const value = match[1];
8772
+ const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
8773
+ const accept = req.headers.get("accept") ?? "";
8774
+ if (accept.includes("application/json")) {
8775
+ return Response.json({ ok: true, theme: value }, { headers: { "Set-Cookie": cookie } });
8776
+ }
8777
+ const referer = req.headers.get("referer") || "/";
8778
+ return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
8779
+ }
8780
+ let themeValue = opts.default;
8781
+ if (opts.cookie) {
8782
+ const fromCookie = getCookies(req)[opts.cookie];
8783
+ if (fromCookie) themeValue = fromCookie;
8784
+ }
8785
+ ctx.theme = {
8786
+ value: themeValue,
8787
+ set: makeSetTheme(opts.cookie, req.headers.get("referer") || "/")
8788
+ };
8789
+ return next(req, ctx);
8790
+ };
8791
+ }
8792
+
8793
+ // i18n.ts
8105
8794
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
8106
8795
  import { join as join7, resolve as resolve13 } from "node:path";
8107
- var defaults = {
8108
- locale: { default: "en", cookie: "locale", fromAcceptLanguage: true },
8109
- theme: { default: "system", cookie: "theme" }
8796
+ var DEFAULTS2 = {
8797
+ default: "en",
8798
+ cookie: "locale",
8799
+ fromAcceptLanguage: true
8110
8800
  };
8111
8801
  function translate(msgs, key, params, fallback) {
8112
8802
  const msg = key.split(".").reduce((o, k) => o?.[k], msgs);
@@ -8118,130 +8808,125 @@ function translate(msgs, key, params, fallback) {
8118
8808
  }
8119
8809
  return result;
8120
8810
  }
8121
- function prefCookie(name, value) {
8122
- return `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
8123
- }
8124
- async function handlePrefSwitch(req, value, cookieName, load) {
8125
- const isJson = req.headers.get("accept")?.includes("application/json");
8126
- if (isJson) {
8127
- const result = { ok: true };
8128
- if (cookieName === "locale" || cookieName === "lang") {
8129
- result.locale = value;
8130
- const messages2 = await load(value);
8131
- if (Object.keys(messages2).length > 0) result.messages = messages2;
8132
- } else {
8133
- result.theme = value;
8134
- }
8135
- return Response.json(result, {
8136
- headers: { "Set-Cookie": prefCookie(cookieName, value) }
8137
- });
8138
- }
8139
- const referer = req.headers.get("referer") || "/";
8140
- return new Response(null, {
8141
- status: 302,
8142
- headers: { Location: referer, "Set-Cookie": prefCookie(cookieName, value) }
8143
- });
8144
- }
8145
- function preferences(options) {
8146
- const dir = options.dir ? resolve13(options.dir) : void 0;
8147
- const localeOpts = { ...defaults.locale, ...options.locale };
8148
- const themeOpts = { ...defaults.theme, ...options.theme };
8811
+ function i18n(options) {
8812
+ const opts = { ...DEFAULTS2, ...options };
8813
+ const dir = opts.dir ? resolve13(opts.dir) : void 0;
8149
8814
  const cache3 = /* @__PURE__ */ new Map();
8150
8815
  function validLocale(locale) {
8151
8816
  return /^[\w-]+$/.test(locale) && !locale.includes("..");
8152
8817
  }
8153
- async function load(locale) {
8154
- if (!dir) return {};
8155
- if (!validLocale(locale)) return {};
8818
+ async function loadMessages(locale) {
8819
+ if (opts.messages?.[locale] && Object.keys(opts.messages[locale]).length > 0) {
8820
+ cache3.set(locale, opts.messages[locale]);
8821
+ return opts.messages[locale];
8822
+ }
8823
+ if (!dir || !validLocale(locale)) return {};
8156
8824
  const cached = cache3.get(locale);
8157
8825
  if (cached) return cached;
8158
8826
  const filePath = join7(dir, `${locale}.json`);
8159
- let data = null;
8160
8827
  try {
8161
8828
  await stat2(filePath);
8162
8829
  const content = await readFile2(filePath, "utf-8");
8163
- data = JSON.parse(content);
8830
+ const data = JSON.parse(content);
8164
8831
  cache3.set(locale, data);
8165
8832
  return data;
8166
8833
  } catch {
8167
8834
  }
8168
- if (!data) {
8169
- const short = locale.split("-")[0];
8170
- if (short !== locale) {
8171
- const fallback = cache3.get(short) || await load(short);
8172
- if (fallback && Object.keys(fallback).length > 0) {
8173
- cache3.set(locale, fallback);
8174
- return fallback;
8175
- }
8835
+ const short = locale.split("-")[0];
8836
+ if (short !== locale) {
8837
+ const fallback = cache3.get(short) || await loadMessages(short);
8838
+ if (fallback && Object.keys(fallback).length > 0) {
8839
+ cache3.set(locale, fallback);
8840
+ return fallback;
8176
8841
  }
8177
8842
  }
8178
8843
  return {};
8179
8844
  }
8845
+ function detectLocale(req) {
8846
+ if (opts.cookie) {
8847
+ const fromCookie = getCookies(req)[opts.cookie];
8848
+ if (fromCookie && validLocale(fromCookie)) return fromCookie;
8849
+ }
8850
+ if (opts.fromAcceptLanguage) {
8851
+ const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.trim();
8852
+ if (fromHeader && validLocale(fromHeader)) return fromHeader;
8853
+ }
8854
+ return opts.default;
8855
+ }
8180
8856
  return async (req, ctx, next) => {
8181
8857
  const url = new URL(req.url);
8182
- const langMatch = url.pathname.match(/^\/__lang\/([\w-]+)$/);
8183
- if (langMatch && req.method === "GET") {
8184
- return handlePrefSwitch(req, langMatch[1], localeOpts.cookie, load);
8185
- }
8186
- const themeMatch = url.pathname.match(/^\/__theme\/([\w-]+)$/);
8187
- if (themeMatch && req.method === "GET") {
8188
- return handlePrefSwitch(req, themeMatch[1], themeOpts.cookie, load);
8189
- }
8190
- const locale = detectLocale(req, localeOpts);
8191
- const theme = detectTheme(req, themeOpts);
8192
- ctx.prefs = { locale, theme };
8193
- if (dir) {
8194
- const msgs = await load(locale);
8195
- ctx.t = (key, params, fallback) => translate(msgs, key, params, fallback);
8196
- globalThis.__LOCALE_DATA__ = msgs;
8197
- ctx.parsed = { ...ctx.parsed, __localeData: msgs };
8198
- }
8199
- ctx.setPref = (name, value) => {
8200
- const cookieOpts = [`${name}=${encodeURIComponent(value)}`, "Path=/", "SameSite=Lax"];
8858
+ const match = url.pathname.match(/^\/__lang\/([\w-]+)$/);
8859
+ if (match && req.method === "GET") {
8860
+ const value = match[1];
8861
+ const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
8862
+ const messages2 = await loadMessages(value);
8863
+ const accept = req.headers.get("accept") ?? "";
8864
+ if (accept.includes("application/json")) {
8865
+ return Response.json(
8866
+ { ok: true, locale: value, messages: Object.keys(messages2).length > 0 ? messages2 : void 0 },
8867
+ { headers: { "Set-Cookie": cookie } }
8868
+ );
8869
+ }
8201
8870
  const referer = req.headers.get("referer") || "/";
8202
- return new Response(null, {
8203
- status: 302,
8204
- headers: {
8205
- Location: referer,
8206
- "Set-Cookie": cookieOpts.join("; ")
8207
- }
8208
- });
8871
+ return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
8872
+ }
8873
+ const locale = detectLocale(req);
8874
+ const msgs = await loadMessages(locale);
8875
+ ctx.i18n = {
8876
+ locale,
8877
+ messages: msgs,
8878
+ t: (key, params, fallback) => translate(msgs, key, params, fallback),
8879
+ set: (value, loc) => {
8880
+ const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
8881
+ const location = loc ?? (req.headers.get("referer") || "/");
8882
+ return new Response(null, { status: 302, headers: { Location: location, "Set-Cookie": cookie } });
8883
+ }
8209
8884
  };
8210
- const flashVal = getCookies(req)["flash"] ?? null;
8211
- if (flashVal) {
8885
+ return next(req, ctx);
8886
+ };
8887
+ }
8888
+
8889
+ // flash.ts
8890
+ function makeSetFlash(name, location) {
8891
+ return (data, loc) => {
8892
+ const finalLoc = loc ?? location;
8893
+ const value = encodeURIComponent(JSON.stringify(data));
8894
+ return new Response(null, {
8895
+ status: 302,
8896
+ headers: {
8897
+ Location: finalLoc,
8898
+ "Set-Cookie": `${name}=${value}; Path=/; SameSite=Lax`
8899
+ }
8900
+ });
8901
+ };
8902
+ }
8903
+ function flash(options) {
8904
+ const name = options?.name ?? "flash";
8905
+ return async (req, ctx, next) => {
8906
+ const raw = getCookies(req)[name] ?? null;
8907
+ const referer = req.headers.get("referer") || "/";
8908
+ let value = void 0;
8909
+ if (raw) {
8212
8910
  try {
8213
- ctx.prefs.flash = JSON.parse(flashVal);
8911
+ value = JSON.parse(decodeURIComponent(raw));
8214
8912
  } catch {
8215
- ctx.prefs.flash = flashVal;
8913
+ value = raw;
8216
8914
  }
8217
8915
  }
8916
+ ;
8917
+ ctx.flash = {
8918
+ value,
8919
+ set: makeSetFlash(name, referer)
8920
+ };
8218
8921
  const res = await next(req, ctx);
8219
- if (flashVal) {
8922
+ if (raw) {
8220
8923
  const headers = new Headers(res.headers);
8221
- headers.append("Set-Cookie", "flash=; Path=/; Max-Age=0");
8924
+ headers.append("Set-Cookie", `${name}=; Path=/; Max-Age=0`);
8222
8925
  return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
8223
8926
  }
8224
8927
  return res;
8225
8928
  };
8226
8929
  }
8227
- function detectLocale(req, opts) {
8228
- if (opts.cookie) {
8229
- const fromCookie = getCookies(req)[opts.cookie];
8230
- if (fromCookie) return fromCookie;
8231
- }
8232
- if (opts.fromAcceptLanguage) {
8233
- const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.trim();
8234
- if (fromHeader) return fromHeader;
8235
- }
8236
- return opts.default;
8237
- }
8238
- function detectTheme(req, opts) {
8239
- if (opts.cookie) {
8240
- const fromCookie = getCookies(req)[opts.cookie];
8241
- if (fromCookie) return fromCookie;
8242
- }
8243
- return opts.default;
8244
- }
8245
8930
 
8246
8931
  // seo.ts
8247
8932
  function escapeXml(s) {
@@ -8620,7 +9305,7 @@ function logdb(options) {
8620
9305
  }
8621
9306
 
8622
9307
  // iii/client.ts
8623
- import crypto6 from "node:crypto";
9308
+ import crypto7 from "node:crypto";
8624
9309
 
8625
9310
  // iii/stream.ts
8626
9311
  function notify(channels, stream, group, item, event, data) {
@@ -9149,7 +9834,7 @@ function iii(opts = {}) {
9149
9834
  registerBuiltin("stream::send", (p) => stream.send(p.stream_name, p.group_id, p.type, p.data, p.id));
9150
9835
  registerBuiltin("stream::update", (p) => stream.update(p.stream_name, p.group_id, p.item_id, p.ops));
9151
9836
  function addLocalWorker(worker) {
9152
- const workerId = crypto6.randomUUID();
9837
+ const workerId = crypto7.randomUUID();
9153
9838
  const reg = {
9154
9839
  id: workerId,
9155
9840
  name: worker.name,
@@ -9164,7 +9849,7 @@ function iii(opts = {}) {
9164
9849
  const triggerIds = [];
9165
9850
  for (const t of worker.getTriggers()) {
9166
9851
  if (t.input.function_id === fn.id) {
9167
- const tid = crypto6.randomUUID();
9852
+ const tid = crypto7.randomUUID();
9168
9853
  triggers.set(tid, {
9169
9854
  id: tid,
9170
9855
  type: t.input.type,
@@ -9193,7 +9878,7 @@ function iii(opts = {}) {
9193
9878
  if (!worker) return;
9194
9879
  const handler = async (payload) => {
9195
9880
  if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
9196
- const invocationId = crypto6.randomUUID();
9881
+ const invocationId = crypto7.randomUUID();
9197
9882
  return new Promise((resolve14, reject) => {
9198
9883
  const timer = setTimeout(() => {
9199
9884
  pending.delete(invocationId);
@@ -9228,7 +9913,7 @@ function iii(opts = {}) {
9228
9913
  let engineRef = null;
9229
9914
  const wsHandler = createWsHandler({
9230
9915
  registerRemoteWorker(ws, name) {
9231
- const id2 = crypto6.randomUUID();
9916
+ const id2 = crypto7.randomUUID();
9232
9917
  workers.set(id2, { id: id2, name, ws, functions: [], triggers: [] });
9233
9918
  return id2;
9234
9919
  },
@@ -9239,7 +9924,7 @@ function iii(opts = {}) {
9239
9924
  addRemoteFunction(workerId, id2);
9240
9925
  },
9241
9926
  registerRemoteTrigger(workerId, input) {
9242
- const tid = crypto6.randomUUID();
9927
+ const tid = crypto7.randomUUID();
9243
9928
  const reg = { id: tid, ...input, workerId };
9244
9929
  triggers.set(tid, reg);
9245
9930
  const worker = workers.get(workerId);
@@ -9589,7 +10274,7 @@ function registerWorker(url) {
9589
10274
  }
9590
10275
 
9591
10276
  // session.ts
9592
- import crypto7 from "node:crypto";
10277
+ import crypto8 from "node:crypto";
9593
10278
  var kSaved = /* @__PURE__ */ Symbol("session.saved");
9594
10279
  var kDestroyed = /* @__PURE__ */ Symbol("session.destroyed");
9595
10280
  var kId = /* @__PURE__ */ Symbol("session.id");
@@ -9665,13 +10350,33 @@ var RedisStore = class {
9665
10350
  await this.redis.del(this.key(sid));
9666
10351
  }
9667
10352
  };
9668
- function createSessionObject(data, sid, store2, ttl) {
10353
+ var COOKIE_SEPARATOR = ".";
10354
+ function signSessionId(sid, secret) {
10355
+ const hmac = crypto8.createHmac("sha256", secret).update(sid).digest("base64url").slice(0, 16);
10356
+ return sid + COOKIE_SEPARATOR + hmac;
10357
+ }
10358
+ function unsignSessionId(value, secret) {
10359
+ const dot = value.lastIndexOf(COOKIE_SEPARATOR);
10360
+ if (dot === -1) return null;
10361
+ const sid = value.slice(0, dot);
10362
+ const sig = value.slice(dot + 1);
10363
+ const expected = crypto8.createHmac("sha256", secret).update(sid).digest("base64url").slice(0, 16);
10364
+ if (sig.length !== expected.length) return null;
10365
+ try {
10366
+ return crypto8.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) ? sid : null;
10367
+ } catch {
10368
+ return null;
10369
+ }
10370
+ }
10371
+ var kCreatedAt = "__createdAt";
10372
+ function createSessionObject(data, sid, store2, ttl, createdAt) {
9669
10373
  const obj = data ?? {};
9670
10374
  obj[kSaved] = false;
9671
10375
  obj[kDestroyed] = false;
9672
10376
  obj[kId] = sid;
9673
10377
  obj[kStore] = store2;
9674
10378
  obj[kTtl] = ttl;
10379
+ if (createdAt) obj[kCreatedAt] = createdAt;
9675
10380
  obj.save = () => {
9676
10381
  obj[kSaved] = true;
9677
10382
  };
@@ -9701,6 +10406,8 @@ function isSessionActive(session2) {
9701
10406
  function session(options) {
9702
10407
  const ttl = options?.ttl ?? 24 * 60 * 60 * 1e3;
9703
10408
  const cookieName = options?.cookieName ?? "__session";
10409
+ const secret = options?.secret;
10410
+ const rotateInterval = options?.rotateInterval ?? 9e5;
9704
10411
  const cookieOpts = {
9705
10412
  path: options?.cookie?.path ?? "/",
9706
10413
  domain: options?.cookie?.domain,
@@ -9720,25 +10427,38 @@ function session(options) {
9720
10427
  store2 = mem;
9721
10428
  closeStore = () => mem.close();
9722
10429
  }
10430
+ function writeCookie(res, sid) {
10431
+ const value = secret ? signSessionId(sid, secret) : sid;
10432
+ return setCookie(res, cookieName, value, cookieOpts);
10433
+ }
9723
10434
  const mw = (async (req, ctx, next) => {
9724
10435
  const cookies = getCookies(req);
9725
- const sid = cookies[cookieName];
10436
+ const rawSid = cookies[cookieName];
10437
+ let sid;
10438
+ if (rawSid) {
10439
+ sid = secret ? unsignSessionId(rawSid, secret) : rawSid;
10440
+ }
9726
10441
  let session2;
9727
10442
  let loadedSid = sid ?? null;
10443
+ let needsRotation = false;
9728
10444
  if (sid) {
9729
10445
  const data = await store2.get(sid);
9730
10446
  if (data) {
9731
- session2 = createSessionObject(data, sid, store2, ttl);
10447
+ const createdAt = data[kCreatedAt] ?? Date.now();
10448
+ session2 = createSessionObject(data, sid, store2, ttl, createdAt);
10449
+ if (rotateInterval > 0 && Date.now() - createdAt > rotateInterval) {
10450
+ needsRotation = true;
10451
+ }
9732
10452
  } else {
9733
10453
  loadedSid = null;
9734
- session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
10454
+ session2 = createSessionObject({}, crypto8.randomUUID(), store2, ttl, Date.now());
9735
10455
  }
9736
10456
  } else {
9737
- session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
10457
+ loadedSid = null;
10458
+ session2 = createSessionObject({}, crypto8.randomUUID(), store2, ttl, Date.now());
9738
10459
  }
9739
10460
  const snapshot = isSessionActive(session2) ? JSON.stringify(session2) : null;
9740
10461
  ctx.session = session2;
9741
- ctx.sessionId = session2.id;
9742
10462
  const res = await next(req, ctx);
9743
10463
  const currentSession = ctx.session;
9744
10464
  if (!currentSession || currentSession[kDestroyed]) {
@@ -9747,14 +10467,22 @@ function session(options) {
9747
10467
  }
9748
10468
  return deleteCookie(res, cookieName, cookieOpts);
9749
10469
  }
10470
+ if (needsRotation && loadedSid) {
10471
+ const newId = crypto8.randomUUID();
10472
+ const data = JSON.parse(JSON.stringify(currentSession));
10473
+ data[kCreatedAt] = Date.now();
10474
+ await store2.set(newId, data, ttl);
10475
+ await store2.destroy(loadedSid);
10476
+ loadedSid = newId;
10477
+ currentSession[kId] = newId;
10478
+ currentSession[kCreatedAt] = data[kCreatedAt];
10479
+ }
9750
10480
  const currentData = isSessionActive(currentSession) ? JSON.stringify(currentSession) : null;
9751
10481
  const wasSaved = currentSession[kSaved];
9752
- const changed = wasSaved || currentData !== snapshot;
10482
+ const changed = wasSaved || needsRotation || currentData !== snapshot;
9753
10483
  if (!changed) {
9754
- if (loadedSid) {
9755
- if (store2 instanceof RedisStore) {
9756
- await store2.set(loadedSid, JSON.parse(currentData ?? "{}"), ttl);
9757
- }
10484
+ if (loadedSid && store2 instanceof RedisStore) {
10485
+ await store2.set(loadedSid, JSON.parse(currentData ?? "{}"), ttl);
9758
10486
  }
9759
10487
  return res;
9760
10488
  }
@@ -9763,8 +10491,10 @@ function session(options) {
9763
10491
  const data = JSON.parse(currentData);
9764
10492
  await store2.set(targetSid, data, ttl);
9765
10493
  if (!loadedSid) {
9766
- const cookieRes = setCookie(res, cookieName, targetSid, cookieOpts);
9767
- return cookieRes;
10494
+ return writeCookie(res, targetSid);
10495
+ }
10496
+ if (needsRotation) {
10497
+ return writeCookie(res, targetSid);
9768
10498
  }
9769
10499
  } else if (loadedSid) {
9770
10500
  await store2.destroy(loadedSid);
@@ -9780,7 +10510,7 @@ function session(options) {
9780
10510
  }
9781
10511
 
9782
10512
  // cache.ts
9783
- import crypto8 from "node:crypto";
10513
+ import crypto9 from "node:crypto";
9784
10514
  var BINARY_PREFIXES = [
9785
10515
  "image/",
9786
10516
  "audio/",
@@ -9800,7 +10530,7 @@ function isCacheableStatus(status, allowed) {
9800
10530
  return allowed.includes(status);
9801
10531
  }
9802
10532
  function defaultCacheKey(req) {
9803
- const hash = crypto8.createHash("sha256");
10533
+ const hash = crypto9.createHash("sha256");
9804
10534
  hash.update(req.method);
9805
10535
  hash.update(req.url);
9806
10536
  return hash.digest("hex");
@@ -10015,10 +10745,10 @@ function cache2(options) {
10015
10745
  }
10016
10746
 
10017
10747
  // webhook.ts
10018
- import crypto9 from "node:crypto";
10748
+ import crypto10 from "node:crypto";
10019
10749
  function timingSafeEqual2(a, b) {
10020
10750
  try {
10021
- return crypto9.timingSafeEqual(Buffer.from(a), Buffer.from(b));
10751
+ return crypto10.timingSafeEqual(Buffer.from(a), Buffer.from(b));
10022
10752
  } catch {
10023
10753
  return false;
10024
10754
  }
@@ -10036,7 +10766,7 @@ function createStripeVerifier(config) {
10036
10766
  const signature = parts["v1"];
10037
10767
  if (!timestamp || !signature) return { valid: false, provider: "stripe", event: "", id: void 0 };
10038
10768
  const signed = `${timestamp}.${body}`;
10039
- const expected = crypto9.createHmac("sha256", config.secret).update(signed).digest("hex");
10769
+ const expected = crypto10.createHmac("sha256", config.secret).update(signed).digest("hex");
10040
10770
  const valid = timingSafeEqual2(signature, expected);
10041
10771
  let event = "";
10042
10772
  let id2;
@@ -10053,7 +10783,7 @@ function createGitHubVerifier(config) {
10053
10783
  return (body, headers) => {
10054
10784
  const sig = headers["x-hub-signature-256"];
10055
10785
  if (!sig) return { valid: false, provider: "github", event: "", id: void 0 };
10056
- const expected = `sha256=${crypto9.createHmac("sha256", config.secret).update(body).digest("hex")}`;
10786
+ const expected = `sha256=${crypto10.createHmac("sha256", config.secret).update(body).digest("hex")}`;
10057
10787
  const valid = timingSafeEqual2(sig, expected);
10058
10788
  let event = headers["x-github-event"] ?? "";
10059
10789
  let id2;
@@ -10076,7 +10806,7 @@ function createSlackVerifier(config) {
10076
10806
  return { valid: false, provider: "slack", event: "", id: void 0 };
10077
10807
  }
10078
10808
  const sigBase = `v0:${timestamp}:${body}`;
10079
- const expected = `v0=${crypto9.createHmac("sha256", config.secret).update(sigBase).digest("hex")}`;
10809
+ const expected = `v0=${crypto10.createHmac("sha256", config.secret).update(sigBase).digest("hex")}`;
10080
10810
  const valid = timingSafeEqual2(signature, expected);
10081
10811
  let event = "";
10082
10812
  let id2;
@@ -10237,7 +10967,7 @@ function resolveTableName(table) {
10237
10967
  }
10238
10968
  return name;
10239
10969
  }
10240
- function escapeIdent(s) {
10970
+ function escapeIdent4(s) {
10241
10971
  return `"${s.replace(/"/g, '""')}"`;
10242
10972
  }
10243
10973
  function sqlLit(s) {
@@ -10247,11 +10977,11 @@ async function createIndex(sql2, table, fields, options) {
10247
10977
  const language = options?.language ?? "english";
10248
10978
  const tableName = resolveTableName(table);
10249
10979
  const indexName = options?.indexName ?? `${tableName}_fts_idx`;
10250
- const vectorExpr = fields.map((f) => `coalesce(${escapeIdent(f)}, '')`).join(` || ' ' || `);
10980
+ const vectorExpr = fields.map((f) => `coalesce(${escapeIdent4(f)}, '')`).join(` || ' ' || `);
10251
10981
  const indexType = options?.indexType ?? "gin";
10252
10982
  await sql2.unsafe(`
10253
- CREATE INDEX IF NOT EXISTS ${escapeIdent(indexName)}
10254
- ON ${escapeIdent(tableName)}
10983
+ CREATE INDEX IF NOT EXISTS ${escapeIdent4(indexName)}
10984
+ ON ${escapeIdent4(tableName)}
10255
10985
  USING ${indexType}
10256
10986
  (to_tsvector(${sqlLit(language)}, ${vectorExpr}))
10257
10987
  `);
@@ -10259,7 +10989,7 @@ async function createIndex(sql2, table, fields, options) {
10259
10989
  async function dropIndex(sql2, table, options) {
10260
10990
  const tableName = resolveTableName(table);
10261
10991
  const indexName = options?.indexName ?? `${tableName}_fts_idx`;
10262
- await sql2.unsafe(`DROP INDEX IF EXISTS ${escapeIdent(indexName)}`);
10992
+ await sql2.unsafe(`DROP INDEX IF EXISTS ${escapeIdent4(indexName)}`);
10263
10993
  }
10264
10994
  async function search(sql2, table, query, options) {
10265
10995
  const tableName = resolveTableName(table);
@@ -10273,13 +11003,13 @@ async function search(sql2, table, query, options) {
10273
11003
  }
10274
11004
  const sanitized = query.trim();
10275
11005
  if (!sanitized) return [];
10276
- const vectorExpr = searchFields.map((f) => `coalesce(${escapeIdent(f)}, '')`).join(` || ' ' || `);
11006
+ const vectorExpr = searchFields.map((f) => `coalesce(${escapeIdent4(f)}, '')`).join(` || ' ' || `);
10277
11007
  const langLit = sqlLit(language);
10278
11008
  const queryLit = sqlLit(sanitized);
10279
- const rankColId = escapeIdent(rankCol);
10280
- const tableId = escapeIdent(tableName);
11009
+ const rankColId = escapeIdent4(rankCol);
11010
+ const tableId = escapeIdent4(tableName);
10281
11011
  const headlineExpr = options?.headline ? searchFields.map(
10282
- (f) => `ts_headline(${langLit}, ${escapeIdent(f)}, websearch_to_tsquery(${langLit}, ${queryLit}), 'MaxWords=30,MinWords=15') as ${escapeIdent(f + "_headline")}`
11012
+ (f) => `ts_headline(${langLit}, ${escapeIdent4(f)}, websearch_to_tsquery(${langLit}, ${queryLit}), 'MaxWords=30,MinWords=15') as ${escapeIdent4(f + "_headline")}`
10283
11013
  ).join(",\n ") : "";
10284
11014
  const sql_query = `
10285
11015
  SELECT
@@ -10319,14 +11049,380 @@ async function suggest(sql2, table, prefix, options) {
10319
11049
  const rows = await sql2.unsafe(`
10320
11050
  SELECT DISTINCT ts_lexize(${sqlLit(language)}, word) as tokens
10321
11051
  FROM (
10322
- SELECT regexp_split_to_table(lower(${escapeIdent(field)}), E'\\W+') as word
10323
- FROM ${escapeIdent(tableName)}
11052
+ SELECT regexp_split_to_table(lower(${escapeIdent4(field)}), E'\\W+') as word
11053
+ FROM ${escapeIdent4(tableName)}
10324
11054
  ) words
10325
11055
  WHERE word LIKE ${sqlLit(sanitized + "%")}
10326
11056
  LIMIT ${limit}
10327
11057
  `);
10328
11058
  return rows.map((r) => r.tokens?.[0] ?? "").filter(Boolean);
10329
11059
  }
11060
+
11061
+ // s3.ts
11062
+ import {
11063
+ S3Client,
11064
+ PutObjectCommand,
11065
+ GetObjectCommand,
11066
+ DeleteObjectCommand,
11067
+ HeadObjectCommand,
11068
+ ListObjectsV2Command
11069
+ } from "@aws-sdk/client-s3";
11070
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
11071
+ function s3(options) {
11072
+ const { bucket, publicUrl } = options;
11073
+ const client = new S3Client({
11074
+ region: options.region ?? "us-east-1",
11075
+ endpoint: options.endpoint,
11076
+ forcePathStyle: options.forcePathStyle,
11077
+ credentials: options.credentials
11078
+ });
11079
+ async function put(key, body, putOpts) {
11080
+ const command = new PutObjectCommand({
11081
+ Bucket: bucket,
11082
+ Key: key,
11083
+ Body: body,
11084
+ ContentType: putOpts?.contentType,
11085
+ CacheControl: putOpts?.cacheControl ?? "public, max-age=31536000",
11086
+ Metadata: putOpts?.metadata
11087
+ });
11088
+ await client.send(command);
11089
+ return key;
11090
+ }
11091
+ async function get(key) {
11092
+ try {
11093
+ const command = new GetObjectCommand({ Bucket: bucket, Key: key });
11094
+ const response = await client.send(command);
11095
+ const body = response.Body;
11096
+ if (!body) return null;
11097
+ return Buffer.from(await body.transformToByteArray());
11098
+ } catch (err) {
11099
+ if (err.name === "NoSuchKey") return null;
11100
+ throw err;
11101
+ }
11102
+ }
11103
+ async function del(key) {
11104
+ const command = new DeleteObjectCommand({ Bucket: bucket, Key: key });
11105
+ await client.send(command);
11106
+ }
11107
+ async function exists(key) {
11108
+ try {
11109
+ const command = new HeadObjectCommand({ Bucket: bucket, Key: key });
11110
+ await client.send(command);
11111
+ return true;
11112
+ } catch (err) {
11113
+ if (err.name === "NotFound" || err.name === "NoSuchKey") return false;
11114
+ throw err;
11115
+ }
11116
+ }
11117
+ async function url(key, urlOpts) {
11118
+ const expiresIn = urlOpts?.expiresIn ?? 3600;
11119
+ if (expiresIn === 0) {
11120
+ if (!publicUrl) {
11121
+ throw new Error(
11122
+ "s3.url() with expiresIn=0 requires publicUrl in S3Options. Set publicUrl to enable unsigned public URLs."
11123
+ );
11124
+ }
11125
+ const base = publicUrl.replace(/\/+$/, "");
11126
+ const objectKey = key.startsWith("/") ? key.slice(1) : key;
11127
+ return `${base}/${objectKey}`;
11128
+ }
11129
+ const command = new GetObjectCommand({ Bucket: bucket, Key: key });
11130
+ return getSignedUrl(client, command, { expiresIn });
11131
+ }
11132
+ async function list(prefix) {
11133
+ const keys = [];
11134
+ let continuationToken;
11135
+ do {
11136
+ const command = new ListObjectsV2Command({
11137
+ Bucket: bucket,
11138
+ Prefix: prefix,
11139
+ ContinuationToken: continuationToken
11140
+ });
11141
+ const response = await client.send(command);
11142
+ if (response.Contents) {
11143
+ for (const obj of response.Contents) {
11144
+ if (obj.Key) keys.push(obj.Key);
11145
+ }
11146
+ }
11147
+ continuationToken = response.NextContinuationToken;
11148
+ } while (continuationToken);
11149
+ return keys;
11150
+ }
11151
+ const mod = {
11152
+ put,
11153
+ get,
11154
+ delete: del,
11155
+ exists,
11156
+ url,
11157
+ list,
11158
+ client
11159
+ };
11160
+ const mw = ((req, ctx, next) => {
11161
+ ;
11162
+ ctx.s3 = mod;
11163
+ return next(req, ctx);
11164
+ });
11165
+ mw.put = put;
11166
+ mw.get = get;
11167
+ mw.delete = del;
11168
+ mw.exists = exists;
11169
+ mw.url = url;
11170
+ mw.list = list;
11171
+ mw.client = client;
11172
+ return mw;
11173
+ }
11174
+
11175
+ // kb/index.ts
11176
+ function escapeIdent5(s) {
11177
+ return `"${s.replace(/"/g, '""')}"`;
11178
+ }
11179
+ function knowledgeBase(options) {
11180
+ const { pg, provider, table = "_kb_docs", chunkSize = 512, chunkOverlap = 64, searchLimit = 5, searchThreshold = 0 } = options;
11181
+ const sql2 = pg.sql;
11182
+ const dimension = provider.dimension;
11183
+ const docsTable = pg.table(table, {
11184
+ id: serial("id").primaryKey(),
11185
+ doc_key: text("doc_key").notNull(),
11186
+ title: text("title").notNull().default(""),
11187
+ content: text("content").notNull(),
11188
+ chunk_index: integer("chunk_index").notNull().default(0),
11189
+ metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`),
11190
+ embedding: vector("embedding", dimension),
11191
+ created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
11192
+ });
11193
+ async function migrate() {
11194
+ await sql2.unsafe(`CREATE EXTENSION IF NOT EXISTS "vector"`);
11195
+ await docsTable.create();
11196
+ await docsTable.createIndex("doc_key");
11197
+ await docsTable.createIndex("embedding", { type: "hnsw", operator: "vector_cosine_ops" });
11198
+ }
11199
+ async function ingest(key, content, ingestOpts) {
11200
+ const title = ingestOpts?.title ?? key;
11201
+ const meta = ingestOpts?.metadata ?? {};
11202
+ const cs = ingestOpts?.chunkSize ?? chunkSize;
11203
+ const co = ingestOpts?.chunkOverlap ?? chunkOverlap;
11204
+ const chunks = chunkContent(content, cs, co);
11205
+ const metaJson = JSON.stringify(meta);
11206
+ await sql2.unsafe(`DELETE FROM ${escapeIdent5(table)} WHERE doc_key = $1`, [key]);
11207
+ const embeddings = await Promise.all(chunks.map((c) => provider.embed(c)));
11208
+ for (let i = 0; i < chunks.length; i++) {
11209
+ const vec = `[${embeddings[i].join(",")}]`;
11210
+ await sql2.unsafe(
11211
+ `INSERT INTO ${escapeIdent5(table)} (doc_key, title, content, chunk_index, metadata, embedding)
11212
+ VALUES ($1, $2, $3, $4, $5::jsonb, $6::vector)`,
11213
+ [key, title, chunks[i], i, metaJson, vec]
11214
+ );
11215
+ }
11216
+ return chunks.length;
11217
+ }
11218
+ async function search2(query, searchOpts) {
11219
+ const limit = searchOpts?.limit ?? searchLimit;
11220
+ const threshold = searchOpts?.threshold ?? searchThreshold;
11221
+ const embedding = await provider.embed(query);
11222
+ const vec = `[${embedding.join(",")}]`;
11223
+ const whereClause = threshold > 0 ? `WHERE (1 - (embedding <=> $1::vector) / 2) >= ${threshold}` : "";
11224
+ const rows = await sql2.unsafe(
11225
+ `SELECT id, doc_key, title, content, chunk_index, metadata,
11226
+ 1 - (embedding <=> $1::vector) / 2 AS _score
11227
+ FROM ${escapeIdent5(table)}
11228
+ ${whereClause}
11229
+ ORDER BY embedding <=> $1::vector
11230
+ LIMIT ${limit}`,
11231
+ [vec]
11232
+ );
11233
+ return rows.map((r) => ({
11234
+ id: r.id,
11235
+ key: r.doc_key,
11236
+ title: r.title,
11237
+ content: r.content,
11238
+ score: r._score,
11239
+ metadata: typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata ?? {}
11240
+ }));
11241
+ }
11242
+ async function del(key) {
11243
+ await sql2.unsafe(`DELETE FROM ${escapeIdent5(table)} WHERE doc_key = $1`, [key]);
11244
+ }
11245
+ async function list() {
11246
+ const rows = await sql2.unsafe(`
11247
+ SELECT doc_key, title, COUNT(*) AS chunks
11248
+ FROM ${escapeIdent5(table)}
11249
+ GROUP BY doc_key, title
11250
+ ORDER BY doc_key
11251
+ `);
11252
+ return rows.map((r) => ({
11253
+ key: r.doc_key,
11254
+ title: r.title,
11255
+ chunks: Number(r.chunks)
11256
+ }));
11257
+ }
11258
+ function mw() {
11259
+ return (req, ctx, next) => {
11260
+ ;
11261
+ ctx.kb = { search: search2 };
11262
+ return next(req, ctx);
11263
+ };
11264
+ }
11265
+ return {
11266
+ ingest,
11267
+ search: search2,
11268
+ delete: del,
11269
+ list,
11270
+ migrate,
11271
+ middleware: mw
11272
+ };
11273
+ }
11274
+
11275
+ // permissions.ts
11276
+ function escapeIdent6(s) {
11277
+ return `"${s.replace(/"/g, '""')}"`;
11278
+ }
11279
+ function permissions(options) {
11280
+ const { pg } = options;
11281
+ const sql2 = pg.sql;
11282
+ const prefix = options.prefix ?? "";
11283
+ const rolesTable = `${prefix}_roles`;
11284
+ const rolePermsTable = `${prefix}_role_permissions`;
11285
+ const userRolesTable = `${prefix}_user_roles`;
11286
+ async function migrate() {
11287
+ await sql2.unsafe(`
11288
+ CREATE TABLE IF NOT EXISTS ${escapeIdent6(rolesTable)} (
11289
+ id SERIAL PRIMARY KEY,
11290
+ name TEXT UNIQUE NOT NULL,
11291
+ description TEXT NOT NULL DEFAULT '',
11292
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
11293
+ )
11294
+ `);
11295
+ await sql2.unsafe(`
11296
+ CREATE TABLE IF NOT EXISTS ${escapeIdent6(rolePermsTable)} (
11297
+ id SERIAL PRIMARY KEY,
11298
+ role_id INTEGER NOT NULL REFERENCES ${escapeIdent6(rolesTable)}(id) ON DELETE CASCADE,
11299
+ permission TEXT NOT NULL,
11300
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
11301
+ UNIQUE(role_id, permission)
11302
+ )
11303
+ `);
11304
+ await sql2.unsafe(`
11305
+ CREATE TABLE IF NOT EXISTS ${escapeIdent6(userRolesTable)} (
11306
+ id SERIAL PRIMARY KEY,
11307
+ user_id INTEGER NOT NULL,
11308
+ role_id INTEGER NOT NULL REFERENCES ${escapeIdent6(rolesTable)}(id) ON DELETE CASCADE,
11309
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
11310
+ UNIQUE(user_id, role_id)
11311
+ )
11312
+ `);
11313
+ }
11314
+ async function ensureRole(role) {
11315
+ const [existing] = await sql2.unsafe(
11316
+ `SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $1 LIMIT 1`,
11317
+ [role]
11318
+ );
11319
+ if (existing) return existing.id;
11320
+ const [created] = await sql2.unsafe(
11321
+ `INSERT INTO ${escapeIdent6(rolesTable)} (name) VALUES ($1) RETURNING id`,
11322
+ [role]
11323
+ );
11324
+ return created.id;
11325
+ }
11326
+ async function assignRole(userId, role) {
11327
+ const roleId = await ensureRole(role);
11328
+ await sql2.unsafe(
11329
+ `INSERT INTO ${escapeIdent6(userRolesTable)} (user_id, role_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
11330
+ [userId, roleId]
11331
+ );
11332
+ }
11333
+ async function removeRole(userId, role) {
11334
+ await sql2.unsafe(
11335
+ `DELETE FROM ${escapeIdent6(userRolesTable)} WHERE user_id = $1 AND role_id = (SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $2)`,
11336
+ [userId, role]
11337
+ );
11338
+ }
11339
+ async function grantPermission(role, permission) {
11340
+ const roleId = await ensureRole(role);
11341
+ await sql2.unsafe(
11342
+ `INSERT INTO ${escapeIdent6(rolePermsTable)} (role_id, permission) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
11343
+ [roleId, permission]
11344
+ );
11345
+ }
11346
+ async function revokePermission(role, permission) {
11347
+ await sql2.unsafe(
11348
+ `DELETE FROM ${escapeIdent6(rolePermsTable)} WHERE role_id = (SELECT id FROM ${escapeIdent6(rolesTable)} WHERE name = $1) AND permission = $2`,
11349
+ [role, permission]
11350
+ );
11351
+ }
11352
+ async function getUserRoles(userId) {
11353
+ const rows = await sql2.unsafe(
11354
+ `SELECT r.name FROM ${escapeIdent6(userRolesTable)} ur
11355
+ JOIN ${escapeIdent6(rolesTable)} r ON r.id = ur.role_id
11356
+ WHERE ur.user_id = $1 ORDER BY r.name`,
11357
+ [userId]
11358
+ );
11359
+ return rows.map((r) => r.name);
11360
+ }
11361
+ async function getUserPermissions(userId) {
11362
+ const rows = await sql2.unsafe(
11363
+ `SELECT DISTINCT rp.permission FROM ${escapeIdent6(userRolesTable)} ur
11364
+ JOIN ${escapeIdent6(rolePermsTable)} rp ON rp.role_id = ur.role_id
11365
+ WHERE ur.user_id = $1 ORDER BY rp.permission`,
11366
+ [userId]
11367
+ );
11368
+ return rows.map((r) => r.permission);
11369
+ }
11370
+ const mw = (async (req, ctx, next) => {
11371
+ const userId = ctx.user?.id;
11372
+ let roles = /* @__PURE__ */ new Set();
11373
+ let perms = /* @__PURE__ */ new Set();
11374
+ if (userId) {
11375
+ const userRoles = await getUserRoles(userId);
11376
+ const userPerms = userId ? await getUserPermissions(userId) : [];
11377
+ roles = new Set(userRoles);
11378
+ perms = new Set(userPerms);
11379
+ const hasWildcard = userPerms.includes("*");
11380
+ if (hasWildcard) {
11381
+ perms = /* @__PURE__ */ new Set(["*"]);
11382
+ }
11383
+ }
11384
+ ctx.permissions = { roles, permissions: perms };
11385
+ return next(req, ctx);
11386
+ });
11387
+ function requireRole(...roles) {
11388
+ return (req, ctx, next) => {
11389
+ if (!ctx.permissions?.roles || !roles.some((r) => ctx.permissions.roles.has(r))) {
11390
+ return Response.json(
11391
+ { error: `Forbidden: requires one of roles [${roles.join(", ")}]` },
11392
+ { status: 403 }
11393
+ );
11394
+ }
11395
+ return next(req, ctx);
11396
+ };
11397
+ }
11398
+ function requirePermission(...perms) {
11399
+ return (req, ctx, next) => {
11400
+ const userPerms = ctx.permissions?.permissions;
11401
+ if (!userPerms) {
11402
+ return Response.json({ error: "Forbidden: no permissions loaded" }, { status: 403 });
11403
+ }
11404
+ if (userPerms.has("*")) return next(req, ctx);
11405
+ const missing = perms.filter((p) => !userPerms.has(p));
11406
+ if (missing.length > 0) {
11407
+ return Response.json(
11408
+ { error: `Forbidden: missing permissions [${missing.join(", ")}]` },
11409
+ { status: 403 }
11410
+ );
11411
+ }
11412
+ return next(req, ctx);
11413
+ };
11414
+ }
11415
+ mw.assignRole = assignRole;
11416
+ mw.removeRole = removeRole;
11417
+ mw.grantPermission = grantPermission;
11418
+ mw.revokePermission = revokePermission;
11419
+ mw.getUserRoles = getUserRoles;
11420
+ mw.getUserPermissions = getUserPermissions;
11421
+ mw.requireRole = requireRole;
11422
+ mw.requirePermission = requirePermission;
11423
+ mw.migrate = migrate;
11424
+ return mw;
11425
+ }
10330
11426
  export {
10331
11427
  DEFAULT_MAX_BODY,
10332
11428
  MIGRATIONS_TABLE,
@@ -10339,6 +11435,7 @@ export {
10339
11435
  TestRequest,
10340
11436
  TsxContext,
10341
11437
  agent,
11438
+ aiProvider,
10342
11439
  aiStream,
10343
11440
  analytics,
10344
11441
  auth,
@@ -10346,7 +11443,7 @@ export {
10346
11443
  compress,
10347
11444
  cors,
10348
11445
  createHub,
10349
- createOpenAI,
11446
+ createOpenAI2 as createOpenAI,
10350
11447
  createSSEStream,
10351
11448
  createTestDb,
10352
11449
  createTestServer,
@@ -10359,6 +11456,7 @@ export {
10359
11456
  deploy,
10360
11457
  embed,
10361
11458
  embedMany,
11459
+ flash,
10362
11460
  formatSSE,
10363
11461
  formatSSEData,
10364
11462
  fts_exports as fts,
@@ -10368,9 +11466,11 @@ export {
10368
11466
  graphql,
10369
11467
  health,
10370
11468
  helmet,
11469
+ i18n,
10371
11470
  iii,
10372
11471
  isDev,
10373
11472
  isProd,
11473
+ knowledgeBase,
10374
11474
  loadEnv,
10375
11475
  logdb,
10376
11476
  logger,
@@ -10378,8 +11478,8 @@ export {
10378
11478
  messager,
10379
11479
  openai,
10380
11480
  opencode,
11481
+ permissions,
10381
11482
  postgres,
10382
- preferences,
10383
11483
  queue,
10384
11484
  rateLimit,
10385
11485
  redis,
@@ -10387,6 +11487,7 @@ export {
10387
11487
  requestId,
10388
11488
  runWithTrace,
10389
11489
  runWorkflow,
11490
+ s3,
10390
11491
  seo,
10391
11492
  seoMiddleware,
10392
11493
  seoTags,
@@ -10400,6 +11501,7 @@ export {
10400
11501
  streamText,
10401
11502
  tenant,
10402
11503
  testApp,
11504
+ theme,
10403
11505
  tool2 as tool,
10404
11506
  traceElapsed,
10405
11507
  upload,