weifuwu 0.22.3 → 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
@@ -851,7 +851,7 @@ function sendHttpResponseOnSocket(socket, response) {
851
851
 
852
852
  // tsx-context.ts
853
853
  import { useSyncExternalStore, createContext } from "react";
854
- var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, loaderData: {}, env: {}, user: {} };
854
+ var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, loaderData: {}, env: {}, user: {} };
855
855
  var KEY = "__WEIFUWU_CTX_STORE";
856
856
  function getStore() {
857
857
  if (typeof globalThis !== "undefined" && globalThis[KEY]) {
@@ -859,8 +859,9 @@ function getStore() {
859
859
  }
860
860
  const s = {
861
861
  _ctx: DEFAULT_CTX,
862
- _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 },
863
863
  _listeners: /* @__PURE__ */ new Set(),
864
+ _rebuilders: [],
864
865
  _alsGetStore: null
865
866
  };
866
867
  if (typeof globalThis !== "undefined") {
@@ -873,8 +874,18 @@ function __registerAls(getStore2) {
873
874
  store._alsGetStore = getStore2;
874
875
  }
875
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
+ }
876
883
  store._ctx = { ...store._ctx, ...value };
877
- 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
+ }
878
889
  store._listeners.forEach((fn) => fn());
879
890
  }
880
891
  var TsxContext = createContext(DEFAULT_CTX);
@@ -1085,266 +1096,6 @@ function auth(options) {
1085
1096
  };
1086
1097
  }
1087
1098
 
1088
- // oauth-client.ts
1089
- import crypto2 from "node:crypto";
1090
- import jwt from "jsonwebtoken";
1091
- var BUILTIN_PROVIDERS = {
1092
- google: {
1093
- authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1094
- tokenUrl: "https://oauth2.googleapis.com/token",
1095
- userUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1096
- scope: "openid email profile",
1097
- parseUser: (data) => ({
1098
- id: data.id,
1099
- email: data.email,
1100
- name: data.name,
1101
- avatarUrl: data.picture
1102
- })
1103
- },
1104
- github: {
1105
- authUrl: "https://github.com/login/oauth/authorize",
1106
- tokenUrl: "https://github.com/login/oauth/access_token",
1107
- userUrl: "https://api.github.com/user",
1108
- scope: "read:user user:email",
1109
- parseUser: (data) => ({
1110
- id: String(data.id),
1111
- email: data.email ?? "",
1112
- name: data.name ?? data.login,
1113
- avatarUrl: data.avatar_url
1114
- })
1115
- }
1116
- };
1117
- function oauthClient(options) {
1118
- const {
1119
- pg,
1120
- jwtSecret,
1121
- providers,
1122
- redirectUrl = "/",
1123
- expiresIn = "24h"
1124
- } = options;
1125
- const providerTable = options.table ?? "_auth_providers";
1126
- const router = new Router();
1127
- async function saveOAuthState(ctx, state, provider) {
1128
- if (ctx.session) {
1129
- ctx.session.oauthState = { state, provider };
1130
- }
1131
- }
1132
- function verifyOAuthState(ctx, state, provider) {
1133
- const saved = ctx.session?.oauthState;
1134
- if (!saved) return false;
1135
- if (saved.state !== state || saved.provider !== provider) return false;
1136
- delete ctx.session.oauthState;
1137
- return true;
1138
- }
1139
- async function ensureTable() {
1140
- await pg.sql`
1141
- CREATE TABLE IF NOT EXISTS ${pg.sql(providerTable)} (
1142
- id SERIAL PRIMARY KEY,
1143
- user_id INTEGER NOT NULL REFERENCES "_users"(id) ON DELETE CASCADE,
1144
- provider TEXT NOT NULL,
1145
- provider_id TEXT NOT NULL,
1146
- email TEXT NOT NULL DEFAULT '',
1147
- name TEXT NOT NULL DEFAULT '',
1148
- avatar_url TEXT NOT NULL DEFAULT '',
1149
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1150
- UNIQUE(provider, provider_id)
1151
- )
1152
- `;
1153
- await pg.sql`
1154
- CREATE INDEX IF NOT EXISTS ${pg.sql(providerTable + "_user_idx")}
1155
- ON ${pg.sql(providerTable)}(user_id)
1156
- `;
1157
- }
1158
- async function findUserByProvider(provider, providerId) {
1159
- const [row] = await pg.sql`
1160
- SELECT * FROM ${pg.sql(providerTable)}
1161
- WHERE provider = ${provider} AND provider_id = ${providerId}
1162
- LIMIT 1
1163
- `;
1164
- return row ?? null;
1165
- }
1166
- async function findUserByEmail(email) {
1167
- const [row] = await pg.sql`
1168
- SELECT * FROM "_users" WHERE email = ${email} LIMIT 1
1169
- `;
1170
- return row ?? null;
1171
- }
1172
- async function createUser(email, name) {
1173
- const randomPassword = crypto2.randomBytes(32).toString("hex");
1174
- const [row] = await pg.sql`
1175
- INSERT INTO "_users" (email, password, name, role)
1176
- VALUES (${email}, ${randomPassword}, ${name}, 'user')
1177
- RETURNING *
1178
- `;
1179
- return row;
1180
- }
1181
- async function linkProvider(userId, provider, providerId, email, name, avatarUrl) {
1182
- await pg.sql`
1183
- INSERT INTO ${pg.sql(providerTable)} (user_id, provider, provider_id, email, name, avatar_url)
1184
- VALUES (${userId}, ${provider}, ${providerId}, ${email}, ${name}, ${avatarUrl})
1185
- ON CONFLICT (provider, provider_id) DO NOTHING
1186
- `;
1187
- }
1188
- async function findOrCreateUser(provider, providerId, email, name, avatarUrl) {
1189
- const link = await findUserByProvider(provider, providerId);
1190
- if (link) {
1191
- const [user2] = await pg.sql`SELECT * FROM "_users" WHERE id = ${link.user_id} LIMIT 1`;
1192
- return user2 ?? null;
1193
- }
1194
- if (email) {
1195
- const existingUser = await findUserByEmail(email);
1196
- if (existingUser) {
1197
- await linkProvider(existingUser.id, provider, providerId, email, name, avatarUrl);
1198
- return existingUser;
1199
- }
1200
- }
1201
- const newUser = await createUser(email || `${provider}_${providerId}@oauth.local`, name || provider);
1202
- await linkProvider(newUser.id, provider, providerId, email, name, avatarUrl);
1203
- return newUser;
1204
- }
1205
- function signToken(user2) {
1206
- return jwt.sign(
1207
- { sub: user2.id, email: user2.email, role: user2.role },
1208
- jwtSecret,
1209
- { expiresIn }
1210
- );
1211
- }
1212
- let tableReady = null;
1213
- function ensureInit() {
1214
- if (!tableReady) tableReady = ensureTable();
1215
- return tableReady;
1216
- }
1217
- function getProviderMeta(providerName) {
1218
- const config = providers[providerName];
1219
- if (!config) return null;
1220
- const builtin = BUILTIN_PROVIDERS[providerName];
1221
- const parseUser = config.parseUser ?? builtin?.parseUser;
1222
- if (!parseUser) return null;
1223
- const meta = {
1224
- authUrl: config.authUrl ?? builtin?.authUrl ?? "",
1225
- tokenUrl: config.tokenUrl ?? builtin?.tokenUrl ?? "",
1226
- userUrl: config.userUrl ?? builtin?.userUrl ?? "",
1227
- scope: config.scope ?? builtin?.scope ?? "openid",
1228
- parseUser
1229
- };
1230
- if (!meta.authUrl || !meta.tokenUrl || !meta.userUrl) return null;
1231
- return { config, meta };
1232
- }
1233
- router.get("/:provider", async (req, ctx) => {
1234
- await ensureInit();
1235
- const providerName = ctx.params.provider;
1236
- const resolved = getProviderMeta(providerName);
1237
- if (!resolved) {
1238
- return Response.json({ error: `Unsupported provider: ${providerName}` }, { status: 400 });
1239
- }
1240
- const { config, meta } = resolved;
1241
- const state = crypto2.randomUUID();
1242
- const redirectUri = new URL(req.url);
1243
- redirectUri.pathname = redirectUri.pathname.replace(/\/[^/]+$/, "/") + providerName + "/callback";
1244
- await saveOAuthState(ctx, state, providerName);
1245
- const scope = config.scope ?? meta.scope;
1246
- const params = new URLSearchParams({
1247
- client_id: config.clientId,
1248
- redirect_uri: redirectUri.origin + redirectUri.pathname,
1249
- response_type: "code",
1250
- scope,
1251
- state,
1252
- access_type: "offline",
1253
- prompt: "consent"
1254
- });
1255
- return Response.redirect(`${meta.authUrl}?${params.toString()}`, 302);
1256
- });
1257
- router.get("/:provider/callback", async (req, ctx) => {
1258
- await ensureInit();
1259
- const providerName = ctx.params.provider;
1260
- const resolved = getProviderMeta(providerName);
1261
- if (!resolved) {
1262
- return Response.json({ error: `Unsupported provider: ${providerName}` }, { status: 400 });
1263
- }
1264
- const { config, meta } = resolved;
1265
- const url = new URL(req.url);
1266
- const code = url.searchParams.get("code");
1267
- const state = url.searchParams.get("state");
1268
- if (!code || !state) {
1269
- return Response.json({ error: "Missing code or state parameter" }, { status: 400 });
1270
- }
1271
- if (!verifyOAuthState(ctx, state, providerName)) {
1272
- return Response.json({ error: "Invalid state \u2014 possible CSRF attack" }, { status: 403 });
1273
- }
1274
- const redirectUri = url.origin + url.pathname.replace(/\/callback$/, "");
1275
- let tokenRes;
1276
- try {
1277
- tokenRes = await fetch(meta.tokenUrl, {
1278
- method: "POST",
1279
- headers: {
1280
- "Content-Type": "application/json",
1281
- "Accept": "application/json"
1282
- },
1283
- body: JSON.stringify({
1284
- code,
1285
- client_id: config.clientId,
1286
- client_secret: config.clientSecret,
1287
- redirect_uri: redirectUri,
1288
- grant_type: "authorization_code"
1289
- })
1290
- });
1291
- } catch (err) {
1292
- console.error(`[oauth] token exchange network error for ${providerName}:`, err);
1293
- return Response.json({ error: "Failed to connect to OAuth provider" }, { status: 502 });
1294
- }
1295
- if (!tokenRes.ok) {
1296
- const errBody = await tokenRes.text();
1297
- console.error(`[oauth] token exchange failed for ${providerName}:`, errBody);
1298
- return Response.json({ error: "Failed to exchange authorization code" }, { status: 502 });
1299
- }
1300
- const tokenData = await tokenRes.json();
1301
- const accessToken = tokenData.access_token;
1302
- if (!accessToken) {
1303
- return Response.json({ error: "No access_token in response" }, { status: 502 });
1304
- }
1305
- let userRes;
1306
- try {
1307
- userRes = await fetch(meta.userUrl, {
1308
- headers: { Authorization: `Bearer ${accessToken}` }
1309
- });
1310
- } catch (err) {
1311
- console.error(`[oauth] user info network error for ${providerName}:`, err);
1312
- return Response.json({ error: "Failed to connect to OAuth provider" }, { status: 502 });
1313
- }
1314
- if (!userRes.ok) {
1315
- return Response.json({ error: "Failed to fetch user profile" }, { status: 502 });
1316
- }
1317
- const userData = await userRes.json();
1318
- const providerUser = meta.parseUser(userData, accessToken);
1319
- const user2 = await findOrCreateUser(
1320
- providerName,
1321
- providerUser.id,
1322
- providerUser.email,
1323
- providerUser.name,
1324
- providerUser.avatarUrl ?? ""
1325
- );
1326
- if (!user2) {
1327
- return Response.json({ error: "Failed to create/link user" }, { status: 500 });
1328
- }
1329
- const token = signToken(user2);
1330
- if (ctx.session) {
1331
- ctx.session.userId = user2.id;
1332
- ctx.session.role = user2.role;
1333
- }
1334
- const accept = req.headers.get("accept") ?? "";
1335
- if (accept.includes("application/json")) {
1336
- return Response.json({
1337
- token,
1338
- user: { id: user2.id, email: user2.email, name: user2.name, role: user2.role }
1339
- });
1340
- }
1341
- const finalUrl = new URL(redirectUrl, url.origin);
1342
- finalUrl.searchParams.set("token", token);
1343
- return Response.redirect(finalUrl.toString(), 302);
1344
- });
1345
- return router;
1346
- }
1347
-
1348
1099
  // static.ts
1349
1100
  import { open, realpath } from "node:fs/promises";
1350
1101
  import { extname, resolve as resolve2, normalize, sep } from "node:path";
@@ -1918,10 +1669,10 @@ function helmet(options) {
1918
1669
  }
1919
1670
 
1920
1671
  // request-id.ts
1921
- import crypto3 from "node:crypto";
1672
+ import crypto2 from "node:crypto";
1922
1673
  function requestId(options) {
1923
1674
  const header = options?.header ?? "X-Request-ID";
1924
- const gen = options?.generator ?? (() => crypto3.randomUUID());
1675
+ const gen = options?.generator ?? (() => crypto2.randomUUID());
1925
1676
  return async (req, ctx, next) => {
1926
1677
  const existing = req.headers.get(header);
1927
1678
  const id2 = existing ?? gen();
@@ -2387,18 +2138,21 @@ async function getStreamObject() {
2387
2138
  if (!_ai.streamObject) _ai.streamObject = (await import("ai")).streamObject;
2388
2139
  return _ai.streamObject;
2389
2140
  }
2390
- async function aiStream(handler) {
2141
+ async function aiStream(handler, provider) {
2391
2142
  const r = new Router();
2392
2143
  r.post("/", async (req, ctx) => {
2393
2144
  const options = await handler(req, ctx);
2145
+ if (provider && !options.model) {
2146
+ options.model = provider.model();
2147
+ }
2394
2148
  if (options.schema) {
2395
2149
  const streamObject2 = await getStreamObject();
2396
2150
  const { schema, ...params } = options;
2397
2151
  const result2 = streamObject2({ ...params, schema, output: "object" });
2398
2152
  return result2.toTextStreamResponse();
2399
2153
  }
2400
- const streamText4 = await getStreamText();
2401
- const result = streamText4(options);
2154
+ const streamText3 = await getStreamText();
2155
+ const result = streamText3(options);
2402
2156
  return result.toTextStreamResponse();
2403
2157
  });
2404
2158
  return r;
@@ -2610,22 +2364,21 @@ function runWorkflow(opts = {}) {
2610
2364
  let nodes;
2611
2365
  if (input.nodes && input.nodes.length > 0) {
2612
2366
  nodes = input.nodes;
2613
- } 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"');
2614
2369
  const toolsDesc = Object.entries(opts.tools ?? {}).map(([k, t]) => `- ${k}: ${t.description}`).join("\n");
2615
- const result = await generateText({
2616
- model: opts.model,
2617
- system: [
2618
- "You are a workflow generator. Given a user goal and available tools, output a workflow JSON.",
2619
- "",
2620
- "Available tools:",
2621
- toolsDesc,
2622
- "",
2623
- "Node types: eval (expression), set (variable), get (variable), if (condition), while (loop), call (tool), http (request).",
2624
- "Reference syntax: $var.name, $nodes.id.output, $nodes.id.output.field, $input.field",
2625
- "Output ONLY valid JSON. No explanation, no markdown."
2626
- ].filter(Boolean).join("\n"),
2627
- messages: [{ role: "user", content: input.goal }]
2628
- });
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 });
2629
2382
  const text2 = result.text.trim();
2630
2383
  const jsonStart = text2.indexOf("{");
2631
2384
  const jsonEnd = text2.lastIndexOf("}");
@@ -2633,8 +2386,6 @@ function runWorkflow(opts = {}) {
2633
2386
  const parsed = JSON.parse(text2.slice(jsonStart, jsonEnd + 1));
2634
2387
  nodes = parsed.nodes ?? parsed.workflow?.nodes ?? [];
2635
2388
  if (!Array.isArray(nodes)) throw new Error("Generated workflow has no nodes array");
2636
- } else {
2637
- throw new Error('Provide either "nodes" or a "model" to generate the workflow from "goal"');
2638
2389
  }
2639
2390
  const ctx = {
2640
2391
  variables: /* @__PURE__ */ new Map(),
@@ -2654,6 +2405,54 @@ function runWorkflow(opts = {}) {
2654
2405
  });
2655
2406
  }
2656
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
+
2657
2456
  // ai-sdk.ts
2658
2457
  import {
2659
2458
  streamText,
@@ -2667,7 +2466,7 @@ import {
2667
2466
  } from "ai";
2668
2467
  import {
2669
2468
  openai,
2670
- createOpenAI
2469
+ createOpenAI as createOpenAI2
2671
2470
  } from "@ai-sdk/openai";
2672
2471
 
2673
2472
  // postgres/client.ts
@@ -3296,12 +3095,12 @@ var PgModule = class {
3296
3095
 
3297
3096
  // user/client.ts
3298
3097
  import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
3299
- import jwt3 from "jsonwebtoken";
3098
+ import jwt2 from "jsonwebtoken";
3300
3099
  import { z as z2 } from "zod";
3301
3100
 
3302
3101
  // user/oauth2.ts
3303
- import crypto4 from "node:crypto";
3304
- import jwt2 from "jsonwebtoken";
3102
+ import crypto3 from "node:crypto";
3103
+ import jwt from "jsonwebtoken";
3305
3104
  function createOAuth2Server(deps) {
3306
3105
  const { pg, users, jwtSecret, expiresIn } = deps;
3307
3106
  async function getClient(clientId) {
@@ -3319,8 +3118,8 @@ function createOAuth2Server(deps) {
3319
3118
  };
3320
3119
  }
3321
3120
  async function registerClient(data) {
3322
- const clientId = crypto4.randomUUID();
3323
- const clientSecret = crypto4.randomBytes(32).toString("hex");
3121
+ const clientId = crypto3.randomUUID();
3122
+ const clientSecret = crypto3.randomBytes(32).toString("hex");
3324
3123
  const [row] = await pg.sql`
3325
3124
  INSERT INTO "_oauth2_clients" ("name", "client_id", "client_secret", "redirect_uris")
3326
3125
  VALUES (${data.name}, ${clientId}, ${clientSecret}, ${pg.sql.array(data.redirectUris)})
@@ -3344,7 +3143,7 @@ function createOAuth2Server(deps) {
3344
3143
  const header = req.headers.get("Authorization");
3345
3144
  if (header?.startsWith("Bearer ")) {
3346
3145
  try {
3347
- const payload = jwt2.verify(header.slice(7), jwtSecret);
3146
+ const payload = jwt.verify(header.slice(7), jwtSecret);
3348
3147
  return { id: payload.sub, email: payload.email, role: payload.role };
3349
3148
  } catch {
3350
3149
  return null;
@@ -3354,7 +3153,7 @@ function createOAuth2Server(deps) {
3354
3153
  const qsToken = url.searchParams.get("access_token");
3355
3154
  if (qsToken) {
3356
3155
  try {
3357
- const payload = jwt2.verify(qsToken, jwtSecret);
3156
+ const payload = jwt.verify(qsToken, jwtSecret);
3358
3157
  return { id: payload.sub, email: payload.email, role: payload.role };
3359
3158
  } catch {
3360
3159
  return null;
@@ -3365,7 +3164,7 @@ function createOAuth2Server(deps) {
3365
3164
  const match = cookie.split(";").map((c) => c.trim()).find((c) => c.startsWith("session="));
3366
3165
  if (match) {
3367
3166
  try {
3368
- const payload = jwt2.verify(match.slice(8), jwtSecret);
3167
+ const payload = jwt.verify(match.slice(8), jwtSecret);
3369
3168
  return { id: payload.sub, email: payload.email, role: payload.role };
3370
3169
  } catch {
3371
3170
  return null;
@@ -3468,7 +3267,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
3468
3267
  const loc2 = `${redirectUri}?error=access_denied${state ? `&state=${state}` : ""}`;
3469
3268
  return Response.redirect(loc2, 302);
3470
3269
  }
3471
- const code = crypto4.randomUUID();
3270
+ const code = crypto3.randomUUID();
3472
3271
  const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
3473
3272
  await pg.sql`
3474
3273
  INSERT INTO "_oauth2_codes" ("code", "client_id", "user_id", "redirect_uri", "code_challenge", "code_challenge_method", "scope", "expires_at")
@@ -3533,7 +3332,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
3533
3332
  if (stored.code_challenge_method === "plain") {
3534
3333
  expected = codeVerifier;
3535
3334
  } else {
3536
- expected = crypto4.createHash("sha256").update(codeVerifier).digest().toString("base64url");
3335
+ expected = crypto3.createHash("sha256").update(codeVerifier).digest().toString("base64url");
3537
3336
  }
3538
3337
  if (expected !== stored.code_challenge) {
3539
3338
  return Response.json({ error: "invalid_grant", error_description: "code_verifier mismatch" }, { status: 400 });
@@ -3545,12 +3344,12 @@ h2{color:#dc2626}.desc{color:#555}</style>
3545
3344
  return Response.json({ error: "invalid_grant" }, { status: 400 });
3546
3345
  }
3547
3346
  const scope = stored.scope || "";
3548
- const accessToken = jwt2.sign(
3347
+ const accessToken = jwt.sign(
3549
3348
  { sub: user2.id, email: user2.email, role: user2.role, client_id: clientId, scope },
3550
3349
  jwtSecret,
3551
3350
  { expiresIn }
3552
3351
  );
3553
- const refreshToken = crypto4.randomUUID();
3352
+ const refreshToken = crypto3.randomUUID();
3554
3353
  const refreshExpires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3);
3555
3354
  await pg.sql`
3556
3355
  INSERT INTO "_oauth2_tokens" ("token", "client_id", "user_id", "scope", "expires_at")
@@ -3572,7 +3371,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
3572
3371
  if (!client || client.clientSecret !== clientSecret) {
3573
3372
  return Response.json({ error: "invalid_client" }, { status: 401 });
3574
3373
  }
3575
- const accessToken = jwt2.sign(
3374
+ const accessToken = jwt.sign(
3576
3375
  { sub: clientId, client_id: clientId, scope, token_type: "client_credentials" },
3577
3376
  jwtSecret,
3578
3377
  { expiresIn }
@@ -3587,32 +3386,255 @@ h2{color:#dc2626}.desc{color:#555}</style>
3587
3386
  return { authorizeHandler, consentHandler, tokenHandler, registerClient, getClient, revokeClient };
3588
3387
  }
3589
3388
 
3590
- // user/client.ts
3591
- var RegisterSchema = z2.object({
3592
- email: z2.string().email(),
3593
- password: z2.string().min(6),
3594
- name: z2.string().min(1)
3595
- });
3596
- var LoginSchema = z2.object({
3597
- email: z2.string().email(),
3598
- password: z2.string().min(1)
3599
- });
3600
- function hashPassword(password) {
3601
- const salt = randomBytes(16).toString("hex");
3602
- const hash = scryptSync(password, salt, 64).toString("hex");
3603
- return `${salt}:${hash}`;
3604
- }
3605
- function verifyPassword(password, stored) {
3606
- const [salt, hash] = stored.split(":");
3607
- const verify = scryptSync(password, salt, 64).toString("hex");
3608
- if (hash.length !== verify.length) return false;
3609
- return timingSafeEqual(Buffer.from(hash), Buffer.from(verify));
3610
- }
3611
- function user(options) {
3612
- const table = options.table ?? "_users";
3613
- const pg = options.pg;
3614
- const secret = options.jwtSecret;
3615
- const expiresIn = options.expiresIn ?? "24h";
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
+
3609
+ // user/client.ts
3610
+ var RegisterSchema = z2.object({
3611
+ email: z2.string().email(),
3612
+ password: z2.string().min(6),
3613
+ name: z2.string().min(1)
3614
+ });
3615
+ var LoginSchema = z2.object({
3616
+ email: z2.string().email(),
3617
+ password: z2.string().min(1)
3618
+ });
3619
+ function escapeIdent2(s) {
3620
+ return `"${s.replace(/"/g, '""')}"`;
3621
+ }
3622
+ function hashPassword(password) {
3623
+ const salt = randomBytes(16).toString("hex");
3624
+ const hash = scryptSync(password, salt, 64).toString("hex");
3625
+ return `${salt}:${hash}`;
3626
+ }
3627
+ function verifyPassword(password, stored) {
3628
+ const [salt, hash] = stored.split(":");
3629
+ const verify = scryptSync(password, salt, 64).toString("hex");
3630
+ if (hash.length !== verify.length) return false;
3631
+ return timingSafeEqual(Buffer.from(hash), Buffer.from(verify));
3632
+ }
3633
+ function user(options) {
3634
+ const table = options.table ?? "_users";
3635
+ const pg = options.pg;
3636
+ const secret = options.jwtSecret;
3637
+ const expiresIn = options.expiresIn ?? "24h";
3616
3638
  const oauth2Enabled = options.oauth2?.server ?? false;
3617
3639
  const base = new PgModule(pg);
3618
3640
  const users = pg.table(table, {
@@ -3630,6 +3652,25 @@ function user(options) {
3630
3652
  }
3631
3653
  async function migrate() {
3632
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
+ }
3633
3674
  if (!oauth2Enabled) return;
3634
3675
  const clients3 = pg.table("_oauth2_clients", {
3635
3676
  id: serial("id").primaryKey(),
@@ -3666,7 +3707,7 @@ function user(options) {
3666
3707
  await tokens.create();
3667
3708
  }
3668
3709
  function signToken(user2) {
3669
- return jwt3.sign(
3710
+ return jwt2.sign(
3670
3711
  { sub: user2.id, email: user2.email, role: user2.role },
3671
3712
  secret,
3672
3713
  { expiresIn }
@@ -3683,6 +3724,11 @@ function user(options) {
3683
3724
  async function findById(id2) {
3684
3725
  return await users.read(id2);
3685
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
+ }
3686
3732
  async function register(data) {
3687
3733
  const { email, password, name } = RegisterSchema.parse(data);
3688
3734
  const existing = await findByEmail(email);
@@ -3717,7 +3763,7 @@ function user(options) {
3717
3763
  }
3718
3764
  async function verify(token) {
3719
3765
  try {
3720
- const payload = jwt3.verify(token, secret);
3766
+ const payload = jwt2.verify(token, secret);
3721
3767
  if (payload.token_type === "client_credentials") return null;
3722
3768
  const row = await findById(payload.sub);
3723
3769
  if (!row) return null;
@@ -3799,6 +3845,20 @@ function user(options) {
3799
3845
  return r2;
3800
3846
  }
3801
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
+ }
3802
3862
  const mod = r;
3803
3863
  mod.middleware = middleware;
3804
3864
  mod.migrate = migrate;
@@ -3837,29 +3897,8 @@ function redis(opts) {
3837
3897
  // queue/index.ts
3838
3898
  import { Redis as IORedis2 } from "ioredis";
3839
3899
  import crypto5 from "node:crypto";
3840
- function cronNext(expr, from = /* @__PURE__ */ new Date()) {
3841
- const parts = expr.trim().split(/\s+/);
3842
- if (parts.length !== 5) throw new Error(`Invalid cron expression "${expr}": expected 5 fields`);
3843
- const fields = parts.map((f, i) => {
3844
- const ranges = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
3845
- const [min, max] = ranges[i];
3846
- return parseField(f, min, max);
3847
- });
3848
- let candidate = new Date(from.getTime() + 6e4);
3849
- candidate.setSeconds(0, 0);
3850
- for (let i = 0; i < 525600; i++) {
3851
- const m = candidate.getMonth() + 1;
3852
- const d = candidate.getDate();
3853
- const h = candidate.getHours();
3854
- const min = candidate.getMinutes();
3855
- const dw = candidate.getDay();
3856
- if (fields[4].has(dw) && fields[3].has(m) && fields[2].has(d) && fields[1].has(h) && fields[0].has(min)) {
3857
- return candidate.getTime();
3858
- }
3859
- candidate.setTime(candidate.getTime() + 6e4);
3860
- }
3861
- throw new Error(`No future date found for cron expression "${expr}"`);
3862
- }
3900
+
3901
+ // cron-utils.ts
3863
3902
  function parseField(field, min, max) {
3864
3903
  const values = /* @__PURE__ */ new Set();
3865
3904
  for (const part of field.split(",")) {
@@ -3868,19 +3907,23 @@ function parseField(field, min, max) {
3868
3907
  } else if (part.includes("/")) {
3869
3908
  const [range, stepStr] = part.split("/");
3870
3909
  const step = parseInt(stepStr, 10);
3910
+ if (isNaN(step) || step < 1) throw new Error(`Invalid cron step: ${part}`);
3871
3911
  let start = min;
3872
3912
  let end = max;
3873
3913
  if (range !== "*") {
3874
- const parts = range.split("-");
3875
- start = parseInt(parts[0], 10);
3876
- 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;
3877
3917
  }
3878
3918
  for (let i = start; i <= end; i += step) values.add(i);
3879
3919
  } else if (part.includes("-")) {
3880
3920
  const [s, e] = part.split("-").map(Number);
3921
+ if (isNaN(s) || isNaN(e)) throw new Error(`Invalid cron range: ${part}`);
3881
3922
  for (let i = s; i <= e; i++) values.add(i);
3882
3923
  } else {
3883
- 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);
3884
3927
  }
3885
3928
  }
3886
3929
  const result = /* @__PURE__ */ new Set();
@@ -3889,41 +3932,333 @@ function parseField(field, min, max) {
3889
3932
  }
3890
3933
  return result;
3891
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
3892
3973
  function queue(opts) {
3893
- const redis2 = opts?.redis ?? new IORedis2(opts?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379");
3894
- 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) {
3895
3994
  const pollInterval = opts?.pollInterval ?? 200;
3896
3995
  const handlers = /* @__PURE__ */ new Map();
3996
+ const jobs = [];
3997
+ const failed = [];
3998
+ const MAX_FAILED = 1e3;
3897
3999
  let running = false;
3898
4000
  let pollTimer = null;
3899
- let epoch = 0;
3900
4001
  let _processed = 0;
3901
4002
  let _failed = 0;
3902
- const jobKey = `${prefix}:jobs`;
3903
- const failedKey = `${prefix}:failed`;
3904
- 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
+ }
3905
4040
  const mw = ((req, ctx, next) => {
3906
4041
  ctx.queue = q;
3907
4042
  return next(req, ctx);
3908
4043
  });
3909
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;
3910
4123
  const MAX_CONCURRENT = 16;
3911
- let inflight = 0;
3912
- 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) {
4132
+ inflight++;
4133
+ try {
4134
+ await handler(job);
4135
+ _processed++;
4136
+ await sql2.unsafe(`DELETE FROM ${escapeIdent3(table)} WHERE id = $1`, [job.id]);
4137
+ } catch (e) {
4138
+ _failed++;
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) {
3913
4253
  inflight++;
3914
4254
  try {
3915
- await jobHandler(job);
4255
+ await handler(job);
3916
4256
  _processed++;
3917
4257
  } catch (e) {
3918
4258
  _failed++;
3919
- const errMsg = e.message;
3920
- console.error("[queue] handler error:", errMsg);
3921
- const failedEntry = JSON.stringify({
3922
- ...job,
3923
- error: errMsg,
3924
- failedAt: Date.now()
3925
- });
3926
- await redis2.lpush(failedKey, failedEntry);
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() }));
3927
4262
  await redis2.ltrim(failedKey, 0, MAX_FAILED - 1);
3928
4263
  } finally {
3929
4264
  inflight--;
@@ -3931,8 +4266,7 @@ function queue(opts) {
3931
4266
  if (job.schedule) {
3932
4267
  try {
3933
4268
  const nextRun = cronNext(job.schedule);
3934
- const nextJob = { ...job, id: crypto5.randomUUID(), runAt: nextRun, createdAt: Date.now() };
3935
- 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() }));
3936
4270
  } catch (e) {
3937
4271
  console.error("[queue] cron re-queue failed:", e.message);
3938
4272
  }
@@ -3946,8 +4280,7 @@ function queue(opts) {
3946
4280
  while (running && inflight < MAX_CONCURRENT) {
3947
4281
  const result = await redis2.zpopmin(jobKey);
3948
4282
  if (result.length < 2) break;
3949
- const raw = result[0];
3950
- const score = parseInt(result[1], 10);
4283
+ const raw = result[0], score = parseInt(result[1], 10);
3951
4284
  if (score > now) {
3952
4285
  await redis2.zadd(jobKey, score, raw);
3953
4286
  break;
@@ -3958,18 +4291,19 @@ function queue(opts) {
3958
4291
  } catch {
3959
4292
  continue;
3960
4293
  }
3961
- const jobHandler = handlers.get(job.type);
3962
- if (jobHandler) {
3963
- processJob(job, jobHandler);
3964
- }
4294
+ const handler = handlers.get(job.type);
4295
+ if (handler) processJob(job, handler);
3965
4296
  }
3966
4297
  } catch (e) {
3967
4298
  console.error("[queue] poll error:", e.message);
3968
4299
  }
3969
- if (running && currentEpoch === epoch) {
3970
- pollTimer = setTimeout(poll, pollInterval);
3971
- }
4300
+ if (running && currentEpoch === epoch) pollTimer = setTimeout(poll, pollInterval);
3972
4301
  }
4302
+ const mw = ((req, ctx, next) => {
4303
+ ctx.queue = q;
4304
+ return next(req, ctx);
4305
+ });
4306
+ const q = mw;
3973
4307
  mw.add = function add(type, payload, opts2) {
3974
4308
  const id2 = crypto5.randomUUID();
3975
4309
  let runAt;
@@ -4045,8 +4379,8 @@ function queue(opts) {
4045
4379
  return false;
4046
4380
  };
4047
4381
  mw.retryAllFailed = async function retryAllFailed(type) {
4048
- const raw = await redis2.lrange(failedKey, 0, -1);
4049
4382
  let count = 0;
4383
+ const raw = await redis2.lrange(failedKey, 0, -1);
4050
4384
  for (const entry of raw) {
4051
4385
  try {
4052
4386
  const job = JSON.parse(entry);
@@ -4064,52 +4398,43 @@ function queue(opts) {
4064
4398
  return count;
4065
4399
  };
4066
4400
  mw.dashboard = function dashboard() {
4067
- const r = new Router();
4068
- r.get("/", async (req, ctx) => {
4069
- const s = q.stats();
4070
- const pending = await q.jobs(100);
4071
- const byType = {};
4072
- for (const job of pending) {
4073
- if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
4074
- byType[job.type].pending++;
4075
- }
4076
- const failed = await q.failedJobs(1e3);
4077
- for (const job of failed) {
4078
- if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
4079
- byType[job.type].failed++;
4080
- }
4081
- return Response.json({
4082
- stats: s,
4083
- types: byType,
4084
- failedCount: failed.length
4085
- });
4086
- });
4087
- r.get("/:type/failed", async (req, ctx) => {
4088
- const failed = await q.failedJobs(100);
4089
- const filtered = failed.filter((j) => j.type === ctx.params.type);
4090
- return Response.json({ jobs: filtered, count: filtered.length });
4091
- });
4092
- r.post("/:type/retry", async (req, ctx) => {
4093
- const count = await q.retryAllFailed(ctx.params.type);
4094
- return Response.json({ retried: count });
4095
- });
4096
- r.post("/retry/:id", async (req, ctx) => {
4097
- const ok = await q.retryFailed(ctx.params.id);
4098
- if (!ok) return new Response("Job not found", { status: 404 });
4099
- return Response.json({ retried: true });
4100
- });
4101
- return r;
4401
+ return buildDashboard(q);
4102
4402
  };
4103
- mw.stats = () => ({
4104
- running,
4105
- inflight,
4106
- processed: _processed,
4107
- failed: _failed,
4108
- handlers: handlers.size,
4109
- maxConcurrent: MAX_CONCURRENT
4110
- });
4403
+ mw.stats = () => ({ running, inflight, processed: _processed, failed: _failed, handlers: handlers.size, maxConcurrent: MAX_CONCURRENT });
4404
+ attachCron(q, handlers);
4111
4405
  return q;
4112
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
+ }
4113
4438
 
4114
4439
  // tenant/rest.ts
4115
4440
  import { z as z3 } from "zod";
@@ -5017,9 +5342,6 @@ function tenant(options) {
5017
5342
  return mod;
5018
5343
  }
5019
5344
 
5020
- // agent/client.ts
5021
- import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
5022
-
5023
5345
  // agent/rest.ts
5024
5346
  function buildRouter2(deps) {
5025
5347
  const { agents: agentsTable, runs: runsTable, knowledge, runner } = deps;
@@ -5173,12 +5495,10 @@ function buildRouter2(deps) {
5173
5495
  }
5174
5496
 
5175
5497
  // agent/run.ts
5176
- import { streamText as streamText2, generateText as generateText3, embed as embed2 } from "ai";
5177
5498
  import { z as z4 } from "zod";
5178
- function hasKnowledgeDocs(sql2, agentId) {
5179
- return sql2`SELECT 1 FROM "_knowledge_documents" WHERE agent_id = ${agentId} LIMIT 1`.then((r) => r.length > 0);
5180
- }
5181
- function chunkContent(content, chunkSize = 512, overlap = 64) {
5499
+
5500
+ // ai/utils.ts
5501
+ function chunkContent(content, chunkSize, overlap) {
5182
5502
  const paragraphs = content.split(/\n\n+/);
5183
5503
  const chunks = [];
5184
5504
  let current = "";
@@ -5192,8 +5512,13 @@ function chunkContent(content, chunkSize = 512, overlap = 64) {
5192
5512
  if (current) chunks.push(current);
5193
5513
  return chunks;
5194
5514
  }
5195
- async function searchKnowledge(sql2, embedModel, agentId, query, limit = 5) {
5196
- 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);
5197
5522
  const vec = `[${embedding.join(",")}]`;
5198
5523
  const docs = await sql2.unsafe(
5199
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`,
@@ -5206,7 +5531,7 @@ async function loadAgent(agents, agentId) {
5206
5531
  return row ?? null;
5207
5532
  }
5208
5533
  function createRunner(deps) {
5209
- const { sql: sql2, agents, runs, getModel, getEmbeddingModel, userTools } = deps;
5534
+ const { sql: sql2, agents, runs, provider, modelName, userTools } = deps;
5210
5535
  function truncate(s, max = 200) {
5211
5536
  return s.length > max ? s.slice(0, max) + "..." : s;
5212
5537
  }
@@ -5231,8 +5556,6 @@ function createRunner(deps) {
5231
5556
  async function run(agentId, params) {
5232
5557
  const agent2 = await loadAgent(agents, agentId);
5233
5558
  if (!agent2 || !agent2.active) throw new Error("Agent not found or inactive");
5234
- const model = getModel();
5235
- const embedModel = getEmbeddingModel();
5236
5559
  const start = Date.now();
5237
5560
  const hasKB = await hasKnowledgeDocs(sql2, agentId);
5238
5561
  const messages2 = params.messages ?? [];
@@ -5248,7 +5571,7 @@ function createRunner(deps) {
5248
5571
  limit: z4.number().default(5).describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF")
5249
5572
  }),
5250
5573
  execute: async ({ query, limit }) => {
5251
- return searchKnowledge(sql2, embedModel, agentId, query, limit);
5574
+ return searchKnowledge(sql2, provider, agentId, query, limit);
5252
5575
  }
5253
5576
  };
5254
5577
  }
@@ -5259,8 +5582,7 @@ function createRunner(deps) {
5259
5582
  }
5260
5583
  const system = agent2.system_prompt || void 0;
5261
5584
  if (params.stream) {
5262
- const result = streamText2({
5263
- model,
5585
+ const result = provider.streamText({
5264
5586
  system,
5265
5587
  messages: messages2,
5266
5588
  tools: Object.keys(tools).length > 0 ? tools : void 0
@@ -5285,8 +5607,7 @@ function createRunner(deps) {
5285
5607
  });
5286
5608
  return { stream: sseStream };
5287
5609
  } else {
5288
- const result = await generateText3({
5289
- model,
5610
+ const result = await provider.generateText({
5290
5611
  system,
5291
5612
  messages: messages2,
5292
5613
  tools: Object.keys(tools).length > 0 ? tools : void 0
@@ -5305,20 +5626,20 @@ function createRunner(deps) {
5305
5626
  }
5306
5627
  }
5307
5628
  async function addKnowledge(agentId, title, content) {
5308
- const embedModel = getEmbeddingModel();
5309
5629
  const chunks = chunkContent(content);
5310
5630
  const [first] = chunks;
5311
- const { embedding } = await embed2({ model: embedModel, value: first });
5631
+ const embedding = await provider.embed(first);
5312
5632
  const vec = `[${embedding.join(",")}]`;
5313
5633
  const [doc] = await sql2.unsafe(
5314
5634
  `INSERT INTO "_knowledge_documents" ("agent_id", "title", "content", "embedding") VALUES ($1, $2, $3, $4::vector) RETURNING *`,
5315
5635
  [agentId, title, first, vec]
5316
5636
  );
5317
5637
  for (let i = 1; i < chunks.length; i++) {
5318
- const { embedding: emb } = await embed2({ model: embedModel, value: chunks[i] });
5638
+ const emb = await provider.embed(chunks[i]);
5639
+ const vec2 = `[${emb.join(",")}]`;
5319
5640
  await sql2.unsafe(
5320
5641
  `INSERT INTO "_knowledge_documents" ("agent_id", "title", "content", "embedding") VALUES ($1, $2, $3, $4::vector)`,
5321
- [agentId, `${title} (${i + 1})`, chunks[i], `[${emb.join(",")}]`]
5642
+ [agentId, `${title} (${i + 1})`, chunks[i], vec2]
5322
5643
  );
5323
5644
  }
5324
5645
  return doc;
@@ -5327,31 +5648,11 @@ function createRunner(deps) {
5327
5648
  }
5328
5649
 
5329
5650
  // agent/client.ts
5330
- function createModelsFromEnv() {
5331
- const baseURL = process.env.OPENAI_BASE_URL || "http://localhost:11434/v1";
5332
- const apiKey = process.env.OPENAI_API_KEY || "ollama";
5333
- const modelName = process.env.OPENAI_MODEL || "qwen3:0.6b";
5334
- const embedModelName = process.env.OPENAI_EMBEDDING_MODEL || "qwen3-embedding:0.6b";
5335
- const provider = createOpenAI2({ baseURL, apiKey });
5336
- return {
5337
- model: provider(modelName),
5338
- embeddingModel: provider.embedding(embedModelName),
5339
- dimension: parseInt(process.env.EMBEDDING_DIMENSION || "1024", 10)
5340
- };
5341
- }
5342
5651
  function agent(options) {
5343
5652
  const pg = options.pg;
5344
5653
  const sql2 = pg.sql;
5345
- const model = options.model;
5346
- const embeddingModel = options.embeddingModel;
5347
- const dimension = options.embeddingDimension ?? 1024;
5348
- const defaultModels = !model || !embeddingModel ? createModelsFromEnv() : null;
5349
- function getModel() {
5350
- return model ?? defaultModels.model;
5351
- }
5352
- function getEmbeddingModel() {
5353
- return embeddingModel ?? defaultModels.embeddingModel;
5354
- }
5654
+ const resolvedProvider = options.provider ?? aiProvider();
5655
+ const dimension = options.embeddingDimension ?? resolvedProvider.dimension;
5355
5656
  const agentsTable = pg.table("_agents", {
5356
5657
  id: serial("id").primaryKey(),
5357
5658
  tenant_id: text("tenant_id"),
@@ -5388,7 +5689,7 @@ function agent(options) {
5388
5689
  trace_id: text("trace_id"),
5389
5690
  created_at: timestamptz("created_at").notNull().default(sql`NOW()`)
5390
5691
  });
5391
- 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 });
5392
5693
  const base = new PgModule(pg);
5393
5694
  const r = buildRouter2({ agents: agentsTable, runs: runsTable, knowledge: knowledgeTable, runner });
5394
5695
  const mod = r;
@@ -6284,7 +6585,7 @@ import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
6284
6585
  // ssr.ts
6285
6586
  import { createElement as createElement3 } from "react";
6286
6587
  import { createHash as createHash3 } from "node:crypto";
6287
- import { existsSync as existsSync4, readdirSync } from "node:fs";
6588
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "node:fs";
6288
6589
  import { readdir, stat } from "node:fs/promises";
6289
6590
  import { dirname as dirname3, join as join5, resolve as resolve6, relative as relative2 } from "node:path";
6290
6591
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
@@ -6401,6 +6702,7 @@ function compile(path2) {
6401
6702
  return isDev() ? compileTsxDev(path2) : compileTsx(path2);
6402
6703
  }
6403
6704
  var vendorBundle = null;
6705
+ var vendorHash = "";
6404
6706
  async function compileVendorBundle() {
6405
6707
  if (vendorBundle) return vendorBundle;
6406
6708
  if (!_userRequire) _userRequire = createRequire(join2(process.cwd(), "package.json"));
@@ -6415,8 +6717,18 @@ async function compileVendorBundle() {
6415
6717
  const keys = Object.keys(mod).filter((k) => !k.startsWith("_") && k !== "default");
6416
6718
  modules[request] = keys;
6417
6719
  }
6418
- const wfwMod = _userRequire("weifuwu/react");
6419
- 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
+ }
6420
6732
  const used = /* @__PURE__ */ new Set();
6421
6733
  const stmts = [""];
6422
6734
  for (const [request, keys] of Object.entries(modules)) {
@@ -6424,7 +6736,7 @@ async function compileVendorBundle() {
6424
6736
  if (unique.length > 0) stmts.push(`export { ${unique.join(", ")} } from ${JSON.stringify(request)};`);
6425
6737
  }
6426
6738
  const uidWfw = wfwKeys.filter((k) => !used.has(k) && used.add(k));
6427
- 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)};`);
6428
6740
  const result = await esbuild.build({
6429
6741
  stdin: { contents: stmts.join("\n"), resolveDir: process.cwd() },
6430
6742
  format: "esm",
@@ -6432,13 +6744,68 @@ async function compileVendorBundle() {
6432
6744
  write: false
6433
6745
  });
6434
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);
6435
6750
  return vendorBundle;
6436
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
+ }
6437
6789
  async function compileHotComponent(path2) {
6438
6790
  const absPath = resolve3(path2);
6439
6791
  const h = id(absPath);
6440
6792
  const stdin = `import C from ${JSON.stringify(absPath)};
6441
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
+ };
6442
6809
  const result = await esbuild.build({
6443
6810
  stdin: { contents: stdin, loader: "tsx", resolveDir: dirname(absPath) },
6444
6811
  format: "esm",
@@ -6446,7 +6813,8 @@ async function compileHotComponent(path2) {
6446
6813
  jsx: "automatic",
6447
6814
  jsxImportSource: "react",
6448
6815
  bundle: true,
6449
- 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],
6450
6818
  write: false
6451
6819
  });
6452
6820
  let code = new TextDecoder().decode(result.outputFiles[0].contents);
@@ -6471,11 +6839,10 @@ function getPublicEnv() {
6471
6839
  return _publicEnv;
6472
6840
  }
6473
6841
  function buildHeadPayload(opts) {
6474
- const { ctx, base, compiledTailwindCss, isDev: isDev3 } = opts;
6842
+ const { ctx, base, tailwind } = opts;
6475
6843
  let result = "";
6476
- if (isDev3) {
6477
- const vUrl = `${base}/__wfw/v/bundle`;
6478
- result += `<script type="importmap">{
6844
+ const vUrl = `${base}/__wfw/v/bundle?h=${vendorHash}`;
6845
+ result += `<script type="importmap">{
6479
6846
  "imports": {
6480
6847
  "react": "${vUrl}",
6481
6848
  "react-dom": "${vUrl}",
@@ -6485,22 +6852,12 @@ function buildHeadPayload(opts) {
6485
6852
  }
6486
6853
  }</script>
6487
6854
  `;
6488
- }
6489
- if (ctx.prefs?.theme) {
6855
+ if (ctx.theme?.value) {
6490
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>
6491
6857
  `;
6492
6858
  }
6493
- if (compiledTailwindCss) {
6494
- const cssUrl = ctx.tailwindCssUrl;
6495
- if (cssUrl) result += `<link rel="stylesheet" href="${cssUrl}" />
6496
- `;
6497
- }
6498
- const localeData = ctx.parsed?.__localeData ?? globalThis.__LOCALE_DATA__;
6499
- if (localeData && Object.keys(localeData).length > 0) {
6500
- if (!_localeDataCache || _localeDataCache.data !== localeData) {
6501
- _localeDataCache = { data: localeData, json: JSON.stringify(localeData) };
6502
- }
6503
- result += `<script>window.__LOCALE_DATA__=${_localeDataCache.json}</script>
6859
+ if (tailwind?.css) {
6860
+ result += `<link rel="stylesheet" href="${tailwind.url}" />
6504
6861
  `;
6505
6862
  }
6506
6863
  const loaderData = opts.loaderData || {};
@@ -6508,7 +6865,9 @@ function buildHeadPayload(opts) {
6508
6865
  params: ctx.params,
6509
6866
  query: ctx.query,
6510
6867
  parsed: ctx.parsed,
6511
- prefs: ctx.prefs,
6868
+ theme: ctx.theme,
6869
+ i18n: ctx.i18n,
6870
+ flash: ctx.flash,
6512
6871
  loaderData
6513
6872
  };
6514
6873
  if (ctx.user && typeof ctx.user === "object") {
@@ -6528,72 +6887,97 @@ function buildHeadPayload(opts) {
6528
6887
  `;
6529
6888
  return result;
6530
6889
  }
6531
- function buildBodyScripts(opts) {
6890
+ function buildBodyScripts(opts, hydrationScript) {
6532
6891
  const parts = [];
6533
- if (opts.loaderData && Object.keys(opts.loaderData).length > 0) {
6534
- parts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(opts.loaderData)}</script>`);
6535
- }
6536
- if (opts.bundle) {
6537
- parts.push(`<script type="module" src="${opts.base}${opts.bundle.url}"></script>`);
6538
- }
6892
+ if (hydrationScript) parts.push(hydrationScript);
6539
6893
  return parts.join("\n");
6540
6894
  }
6541
- var _localeDataCache = null;
6542
- function streamResponse(reactStream, opts) {
6895
+ function streamResponse(reactStream, opts, hydrationScript) {
6543
6896
  const decoder = new TextDecoder2();
6544
6897
  const encoder2 = new TextEncoder2();
6545
- const headPayload = buildHeadPayload(opts);
6546
- let buffer = "";
6547
- let headFlushed = false;
6548
- let extractedHead = "";
6549
6898
  const output = new ReadableStream({
6550
6899
  async start(controller) {
6551
6900
  try {
6552
6901
  const reader = reactStream.getReader();
6553
- async function push(chunk) {
6554
- buffer += decoder.decode(chunk, { stream: true });
6555
- if (!extractedHead) {
6556
- const m = buffer.match(/<template id="__wfw_head">([\s\S]*?)<\/template>/);
6557
- if (m) {
6558
- extractedHead = m[1];
6559
- buffer = buffer.replace(m[0], "");
6560
- }
6561
- }
6562
- if (!headFlushed) {
6563
- const idx = buffer.indexOf("</head>");
6564
- if (idx !== -1) {
6565
- const before = buffer.slice(0, idx);
6566
- let injection = "";
6567
- if (extractedHead) injection += "\n" + extractedHead;
6568
- injection += headPayload;
6569
- controller.enqueue(encoder2.encode(before + injection));
6570
- buffer = buffer.slice(idx);
6571
- headFlushed = true;
6572
- }
6573
- return;
6574
- }
6575
- controller.enqueue(encoder2.encode(buffer));
6576
- buffer = "";
6577
- }
6902
+ let html = "";
6578
6903
  while (true) {
6579
6904
  const { done, value } = await reader.read();
6580
6905
  if (done) break;
6581
- 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
+ }
6582
6917
  }
6583
- buffer = buffer.replace(/<template id="__wfw_head">[\s\S]*?<\/template>/g, "");
6584
- if (buffer) controller.enqueue(encoder2.encode(buffer));
6585
- const body = buildBodyScripts(opts);
6586
- if (body) controller.enqueue(encoder2.encode("\n" + body));
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);
6922
+ }
6923
+ let bodyScripts = "";
6924
+ const built = buildBodyScripts(opts, hydrationScript);
6925
+ if (built) bodyScripts += built;
6587
6926
  if (opts.isDev) {
6588
6927
  const wsUrl = `${opts.base}/__weifuwu/livereload`;
6589
6928
  const hbUrl = `${opts.base}/__wfw/h/`;
6590
- controller.enqueue(encoder2.encode(
6591
- `
6592
- <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>`
6593
- ));
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>`;
6594
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
+ }
6977
+ }
6978
+ controller.enqueue(encoder2.encode(html));
6595
6979
  } catch {
6596
- 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>';
6597
6981
  controller.enqueue(encoder2.encode(fallback));
6598
6982
  } finally {
6599
6983
  controller.close();
@@ -6623,9 +7007,9 @@ function tailwindContext(dir) {
6623
7007
  await compileTailwindCss(cssPath, cssDir);
6624
7008
  }
6625
7009
  const entry = cssCache.get(cssPath);
6626
- ctx.compiledTailwindCss = entry.css;
6627
7010
  const base = (ctx.mountPath || "").replace(/\/$/, "");
6628
- 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 };
6629
7013
  return next(req, ctx);
6630
7014
  };
6631
7015
  }
@@ -6719,6 +7103,8 @@ function liveWs() {
6719
7103
  }
6720
7104
  function liveRouter(dir) {
6721
7105
  const r = new Router();
7106
+ compileVendorBundle().catch(() => {
7107
+ });
6722
7108
  r.get("/__wfw/v/bundle", async () => {
6723
7109
  const code = await compileVendorBundle();
6724
7110
  return new Response(code, {
@@ -6766,7 +7152,6 @@ function liveWatcher(dir) {
6766
7152
  return broadcastReload();
6767
7153
  }
6768
7154
  clearCompileCache();
6769
- markClientBundleDirty();
6770
7155
  const targets = existsSync3(entryPath) ? [entryPath] : findEntries(resolve5(filePath));
6771
7156
  if (targets.length === 0) return broadcastReload();
6772
7157
  try {
@@ -6872,7 +7257,7 @@ function errorBoundary(errorPath) {
6872
7257
  ctx,
6873
7258
  base,
6874
7259
  isDev: isDev(),
6875
- compiledTailwindCss: ctx.compiledTailwindCss,
7260
+ tailwind: ctx.tailwind,
6876
7261
  status: 500
6877
7262
  });
6878
7263
  }
@@ -6883,25 +7268,6 @@ function errorBoundary(errorPath) {
6883
7268
  var isDev2 = isDev();
6884
7269
  var als2 = new AsyncLocalStorage2();
6885
7270
  __registerAls(() => als2.getStore());
6886
- var bundleCache = /* @__PURE__ */ new Map();
6887
- var _bundleDirty = false;
6888
- function markClientBundleDirty() {
6889
- _bundleDirty = true;
6890
- }
6891
- function getBundle(key) {
6892
- if (_bundleDirty) {
6893
- bundleCache.clear();
6894
- _bundleDirty = false;
6895
- }
6896
- return bundleCache.get(key);
6897
- }
6898
- function setBundle(key, buf) {
6899
- if (_bundleDirty) {
6900
- bundleCache.clear();
6901
- _bundleDirty = false;
6902
- }
6903
- bundleCache.set(key, buf);
6904
- }
6905
7271
  function hashId(s) {
6906
7272
  return createHash3("md5").update(s).digest("hex").slice(0, 8);
6907
7273
  }
@@ -7016,50 +7382,40 @@ async function resolveRoute(ssrDir, segments, routeCache) {
7016
7382
  routeCache.set(cacheKey, result);
7017
7383
  return result;
7018
7384
  }
7019
- async function buildClientBundle(entryPath, layoutPaths) {
7020
- try {
7021
- const absEntry = resolve6(entryPath);
7022
- const absLayouts = layoutPaths.map((p) => resolve6(p));
7023
- const layoutImports = absLayouts.map((p) => `import${JSON.stringify(p)};`).join("");
7024
- 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()})}})()`;
7025
- const code = [
7026
- layoutImports,
7027
- `${isDev2 ? "import{createRoot}from'react-dom/client';" : "import{hydrateRoot}from'react-dom/client';"}`,
7028
- `import{createElement}from'react';`,
7029
- `import{TsxContext}from'weifuwu/react';`,
7030
- `import P from${JSON.stringify(absEntry)};`,
7031
- `var setCtx=${_sc};`,
7032
- `const c=document.getElementById('__weifuwu_root');`,
7033
- `if(window.__WEIFUWU_PROPS)setCtx({loaderData:window.__WEIFUWU_PROPS});`,
7034
- isDev2 ? `const _W=function(props){return(_W._fn||P)(props)};_W._fn=P;const _P=function(props){return createElement(_W,props)};` : "",
7035
- isDev2 ? `window.__WFW_ENTRY=${JSON.stringify(hashId(absEntry))};window.__WFW_REFRESH=function(n){_W._fn=n;window.__WFW_ROOT.render(createElement(App))};` : "",
7036
- `function App(){`,
7037
- `const ctx=window.__WEIFUWU_CTX||{};`,
7038
- `return createElement(TsxContext.Provider,{value:ctx},`,
7039
- isDev2 ? `createElement(_P,null))` : `createElement(P,null))`,
7040
- `}`,
7041
- isDev2 ? `window.__WFW_ROOT=createRoot(c);window.__WFW_ROOT.render(createElement(App));` : `hydrateRoot(c,createElement(App));`
7042
- ].filter(Boolean).join("");
7043
- const { default: esbuild2 } = await import("esbuild");
7044
- const result = await esbuild2.build({
7045
- stdin: { contents: code, loader: "tsx", resolveDir: dirname3(absEntry) },
7046
- bundle: true,
7047
- format: "esm",
7048
- jsx: "automatic",
7049
- jsxImportSource: "react",
7050
- banner: { js: "self.process={env:{}};" },
7051
- loader: { ".node": "empty" },
7052
- external: isDev2 ? ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "weifuwu", "weifuwu/react"] : void 0,
7053
- write: false,
7054
- minify: !isDev2
7055
- });
7056
- return result.outputFiles[0].contents;
7057
- } catch (err) {
7058
- console.error("hydration bundle failed:", err);
7059
- return null;
7060
- }
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
+ `}
7413
+ }
7414
+
7415
+ init();
7416
+ </script>`;
7061
7417
  }
7062
- function renderPage(pageFile) {
7418
+ function renderPage(pageFile, outDir) {
7063
7419
  const absPath = resolve6(pageFile);
7064
7420
  const entryId = hashId(absPath);
7065
7421
  ssrEntries.set(entryId, { path: absPath });
@@ -7085,15 +7441,15 @@ function renderPage(pageFile) {
7085
7441
  query: ctx.query,
7086
7442
  user: ctx.user ?? {},
7087
7443
  parsed: ctx.parsed ?? {},
7088
- prefs: ctx.prefs ?? {},
7444
+ theme: ctx.theme,
7445
+ i18n: ctx.i18n,
7446
+ flash: ctx.flash,
7089
7447
  loaderData,
7090
7448
  env: ctx.env ?? {}
7091
7449
  };
7092
7450
  return als2.run(ctxValue, async () => {
7093
7451
  setCtx(ctxValue);
7094
- if (ctxValue.parsed?.__localeData) {
7095
- globalThis.__LOCALE_DATA__ = ctxValue.parsed.__localeData;
7096
- }
7452
+ await compileBrowser(absPath, outDir);
7097
7453
  let element = createElement3(
7098
7454
  "div",
7099
7455
  { id: "__weifuwu_root" },
@@ -7104,24 +7460,15 @@ function renderPage(pageFile) {
7104
7460
  )
7105
7461
  );
7106
7462
  element = buildHtmlShell("weifuwu", element, layoutComponents);
7107
- let bundle = null;
7108
- if (!getBundle(bundleKey)) {
7109
- const buf = await buildClientBundle(absPath, layoutPaths);
7110
- if (buf) setBundle(bundleKey, buf);
7111
- }
7112
- if (getBundle(bundleKey)) {
7113
- bundle = { url: bundleKey };
7114
- }
7115
7463
  const { renderToReadableStream } = await import("react-dom/server");
7116
7464
  const stream = await renderToReadableStream(element);
7117
7465
  return streamResponse(stream, {
7118
7466
  ctx,
7119
7467
  base,
7120
7468
  isDev: isDev2,
7121
- bundle,
7122
7469
  loaderData,
7123
- compiledTailwindCss: ctx.compiledTailwindCss
7124
- });
7470
+ tailwind: ctx.tailwind
7471
+ }, buildHydrationScript(entryId, JSON.stringify(ctxValue), base));
7125
7472
  });
7126
7473
  };
7127
7474
  }
@@ -7167,11 +7514,23 @@ function discoverRoutes(dir) {
7167
7514
  function ssr(opts) {
7168
7515
  const r = new Router();
7169
7516
  const dir = resolve6(opts.dir);
7517
+ const outDir = resolve6(OUT_DIR);
7170
7518
  const routeCache = /* @__PURE__ */ new Map();
7171
- r.get("/__ssr/:path", (req, ctx) => {
7172
- const buf = getBundle("/__ssr/" + ctx.params.path);
7173
- if (!buf) return new Response("", { status: 404 });
7174
- 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, {
7175
7534
  headers: { "content-type": "application/javascript; charset=utf-8" }
7176
7535
  });
7177
7536
  });
@@ -7198,7 +7557,7 @@ function ssr(opts) {
7198
7557
  ...resolved.layoutFiles.map((f) => layout(f)),
7199
7558
  tailwindContext(dir)
7200
7559
  ];
7201
- const handler = (req2, ctx2) => renderPage(resolved.pageFile)(req2, ctx2);
7560
+ const handler = (req2, ctx2) => renderPage(resolved.pageFile, outDir)(req2, ctx2);
7202
7561
  return runChain(mws, handler, req, ctx);
7203
7562
  });
7204
7563
  const mod = r;
@@ -7288,13 +7647,13 @@ async function addToolMessages(sql2, sessionId, toolCalls, toolResults) {
7288
7647
  }
7289
7648
 
7290
7649
  // opencode/run.ts
7291
- import { streamText as streamText3, stepCountIs } from "ai";
7650
+ import { streamText as streamText2, stepCountIs } from "ai";
7292
7651
  async function* executeGenerator(opts) {
7293
7652
  const { sessionId, input, model, tools, systemPrompt, messages: messages2, sql: sql2, abortSignal } = opts;
7294
7653
  const lastStepToolCalls = [];
7295
7654
  let currentAssistantText = "";
7296
7655
  let currentUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
7297
- const result = streamText3({
7656
+ const result = streamText2({
7298
7657
  model,
7299
7658
  system: systemPrompt,
7300
7659
  messages: [
@@ -7461,7 +7820,7 @@ function createBashTool(ctx) {
7461
7820
  // opencode/tools/read.ts
7462
7821
  import { tool as tool4 } from "ai";
7463
7822
  import { z as z6 } from "zod";
7464
- import { readFileSync as readFileSync4 } from "node:fs";
7823
+ import { readFileSync as readFileSync5 } from "node:fs";
7465
7824
  import { resolve as resolve7 } from "node:path";
7466
7825
  function createReadTool(ctx) {
7467
7826
  return tool4({
@@ -7473,10 +7832,10 @@ function createReadTool(ctx) {
7473
7832
  }),
7474
7833
  execute: async ({ path: path2, offset, limit }) => {
7475
7834
  const resolved = resolve7(ctx.workspace, path2);
7476
- if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
7835
+ if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
7477
7836
  return { error: "Path not allowed", content: null, totalLines: 0 };
7478
7837
  }
7479
- const content = readFileSync4(resolved, "utf-8");
7838
+ const content = readFileSync5(resolved, "utf-8");
7480
7839
  const lines = content.split("\n");
7481
7840
  const totalLines = lines.length;
7482
7841
  if (offset !== void 0) {
@@ -7514,7 +7873,7 @@ function createWriteTool(ctx) {
7514
7873
  }),
7515
7874
  execute: async ({ path: path2, content }) => {
7516
7875
  const resolved = resolve8(ctx.workspace, path2);
7517
- if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
7876
+ if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
7518
7877
  return { error: "Path not allowed" };
7519
7878
  }
7520
7879
  mkdirSync3(dirname4(resolved), { recursive: true });
@@ -7527,7 +7886,7 @@ function createWriteTool(ctx) {
7527
7886
  // opencode/tools/edit.ts
7528
7887
  import { tool as tool6 } from "ai";
7529
7888
  import { z as z8 } from "zod";
7530
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
7889
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
7531
7890
  import { resolve as resolve9 } from "node:path";
7532
7891
  function createEditTool(ctx) {
7533
7892
  return tool6({
@@ -7540,10 +7899,10 @@ function createEditTool(ctx) {
7540
7899
  }),
7541
7900
  execute: async ({ path: path2, oldString, newString, replaceAll }) => {
7542
7901
  const resolved = resolve9(ctx.workspace, path2);
7543
- if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
7902
+ if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
7544
7903
  return { error: "Path not allowed" };
7545
7904
  }
7546
- const content = readFileSync5(resolved, "utf-8");
7905
+ const content = readFileSync6(resolved, "utf-8");
7547
7906
  if (replaceAll) {
7548
7907
  if (!content.includes(oldString)) {
7549
7908
  return { error: "oldString not found in file", replaced: 0 };
@@ -7728,7 +8087,7 @@ ${availableList}
7728
8087
  name: z13.string().describe("The name of the skill to load")
7729
8088
  }),
7730
8089
  execute: async ({ name }) => {
7731
- if (!isSkillAllowed(name, ctx.permissions)) {
8090
+ if (!isSkillAllowed(name, ctx.permissions.permissions)) {
7732
8091
  return { error: `Skill "${name}" is not permitted` };
7733
8092
  }
7734
8093
  const skill = ctx.skillsRegistry.get(name);
@@ -7749,28 +8108,28 @@ ${availableList}
7749
8108
  // opencode/tools/index.ts
7750
8109
  function createTools(ctx) {
7751
8110
  const tools = {};
7752
- if (isToolEnabled("bash", ctx.permissions)) {
8111
+ if (isToolEnabled("bash", ctx.permissions.permissions)) {
7753
8112
  tools.bash = createBashTool(ctx);
7754
8113
  }
7755
- if (isToolEnabled("read", ctx.permissions)) {
8114
+ if (isToolEnabled("read", ctx.permissions.permissions)) {
7756
8115
  tools.read = createReadTool(ctx);
7757
8116
  }
7758
- if (isToolEnabled("write", ctx.permissions)) {
8117
+ if (isToolEnabled("write", ctx.permissions.permissions)) {
7759
8118
  tools.write = createWriteTool(ctx);
7760
8119
  }
7761
- if (isToolEnabled("edit", ctx.permissions)) {
8120
+ if (isToolEnabled("edit", ctx.permissions.permissions)) {
7762
8121
  tools.edit = createEditTool(ctx);
7763
8122
  }
7764
- if (isToolEnabled("grep", ctx.permissions)) {
8123
+ if (isToolEnabled("grep", ctx.permissions.permissions)) {
7765
8124
  tools.grep = createGrepTool(ctx);
7766
8125
  }
7767
- if (isToolEnabled("glob", ctx.permissions)) {
8126
+ if (isToolEnabled("glob", ctx.permissions.permissions)) {
7768
8127
  tools.glob = createGlobTool(ctx);
7769
8128
  }
7770
- if (isToolEnabled("web", ctx.permissions)) {
8129
+ if (isToolEnabled("web", ctx.permissions.permissions)) {
7771
8130
  tools.web = createWebTool(ctx);
7772
8131
  }
7773
- if (ctx.skillsRegistry.all.length > 0 && isToolEnabled("skill", ctx.permissions)) {
8132
+ if (ctx.skillsRegistry.all.length > 0 && isToolEnabled("skill", ctx.permissions.permissions)) {
7774
8133
  tools.skill = createSkillTool(ctx);
7775
8134
  }
7776
8135
  tools.question = createQuestionTool(ctx);
@@ -7779,7 +8138,7 @@ function createTools(ctx) {
7779
8138
 
7780
8139
  // opencode/rest.ts
7781
8140
  async function buildRouter4(deps) {
7782
- const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
8141
+ const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions: permissions2, pendingQuestions } = deps;
7783
8142
  const router = new Router();
7784
8143
  router.post("/sessions", async (req, ctx) => {
7785
8144
  const body = await req.json().catch(() => ({}));
@@ -7814,7 +8173,7 @@ async function buildRouter4(deps) {
7814
8173
  if (!content) return new Response("Missing content", { status: 400 });
7815
8174
  const toolCtx = {
7816
8175
  workspace: session2.workspace || workspace,
7817
- permissions,
8176
+ permissions: permissions2,
7818
8177
  pendingQuestions,
7819
8178
  skillsRegistry: deps.skillsRegistry
7820
8179
  };
@@ -7872,7 +8231,7 @@ async function buildRouter4(deps) {
7872
8231
  // opencode/ws.ts
7873
8232
  var clients2 = /* @__PURE__ */ new WeakMap();
7874
8233
  function createWSHandler2(deps) {
7875
- const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
8234
+ const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions: permissions2, pendingQuestions } = deps;
7876
8235
  return {
7877
8236
  open(ws, ctx) {
7878
8237
  const userId = ctx.user?.id ?? 0;
@@ -7922,7 +8281,7 @@ function createWSHandler2(deps) {
7922
8281
  }
7923
8282
  const toolCtx = {
7924
8283
  workspace: session2.workspace || workspace,
7925
- permissions,
8284
+ permissions: permissions2,
7926
8285
  pendingQuestions,
7927
8286
  skillsRegistry
7928
8287
  };
@@ -8084,7 +8443,7 @@ async function opencode(options) {
8084
8443
  const workspace = options.workspace || process.cwd();
8085
8444
  const systemPrompt = options.systemPrompt;
8086
8445
  const manualSkills = options.skills || [];
8087
- const permissions = options.permissions;
8446
+ const permissions2 = options.permissions;
8088
8447
  const modelName = options.model || "deepseek-v4-flash";
8089
8448
  const [discoveredSkills] = await Promise.all([discoverSkills(workspace)]);
8090
8449
  const skillsRegistry = buildSkillRegistry(discoveredSkills, manualSkills);
@@ -8092,7 +8451,7 @@ async function opencode(options) {
8092
8451
  const model = provider.chat(modelName);
8093
8452
  const pendingQuestions = /* @__PURE__ */ new Map();
8094
8453
  const base = new PgModule(pg);
8095
- 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 });
8096
8455
  const mod = r;
8097
8456
  mod.migrate = async () => {
8098
8457
  const sessions2 = pg.table("_opencode_sessions", {
@@ -8125,7 +8484,7 @@ async function opencode(options) {
8125
8484
  await messages2.create();
8126
8485
  await messages2.createIndex(["session_id", "created_at"]);
8127
8486
  };
8128
- 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 });
8129
8488
  mod.close = () => base.close();
8130
8489
  return mod;
8131
8490
  }
@@ -8395,12 +8754,49 @@ function analytics(options) {
8395
8754
  return mod;
8396
8755
  }
8397
8756
 
8398
- // 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
8399
8794
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
8400
8795
  import { join as join7, resolve as resolve13 } from "node:path";
8401
- var defaults = {
8402
- locale: { default: "en", cookie: "locale", fromAcceptLanguage: true },
8403
- theme: { default: "system", cookie: "theme" }
8796
+ var DEFAULTS2 = {
8797
+ default: "en",
8798
+ cookie: "locale",
8799
+ fromAcceptLanguage: true
8404
8800
  };
8405
8801
  function translate(msgs, key, params, fallback) {
8406
8802
  const msg = key.split(".").reduce((o, k) => o?.[k], msgs);
@@ -8412,130 +8808,125 @@ function translate(msgs, key, params, fallback) {
8412
8808
  }
8413
8809
  return result;
8414
8810
  }
8415
- function prefCookie(name, value) {
8416
- return `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
8417
- }
8418
- async function handlePrefSwitch(req, value, cookieName, load) {
8419
- const isJson = req.headers.get("accept")?.includes("application/json");
8420
- if (isJson) {
8421
- const result = { ok: true };
8422
- if (cookieName === "locale" || cookieName === "lang") {
8423
- result.locale = value;
8424
- const messages2 = await load(value);
8425
- if (Object.keys(messages2).length > 0) result.messages = messages2;
8426
- } else {
8427
- result.theme = value;
8428
- }
8429
- return Response.json(result, {
8430
- headers: { "Set-Cookie": prefCookie(cookieName, value) }
8431
- });
8432
- }
8433
- const referer = req.headers.get("referer") || "/";
8434
- return new Response(null, {
8435
- status: 302,
8436
- headers: { Location: referer, "Set-Cookie": prefCookie(cookieName, value) }
8437
- });
8438
- }
8439
- function preferences(options) {
8440
- const dir = options.dir ? resolve13(options.dir) : void 0;
8441
- const localeOpts = { ...defaults.locale, ...options.locale };
8442
- 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;
8443
8814
  const cache3 = /* @__PURE__ */ new Map();
8444
8815
  function validLocale(locale) {
8445
8816
  return /^[\w-]+$/.test(locale) && !locale.includes("..");
8446
8817
  }
8447
- async function load(locale) {
8448
- if (!dir) return {};
8449
- 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 {};
8450
8824
  const cached = cache3.get(locale);
8451
8825
  if (cached) return cached;
8452
8826
  const filePath = join7(dir, `${locale}.json`);
8453
- let data = null;
8454
8827
  try {
8455
8828
  await stat2(filePath);
8456
8829
  const content = await readFile2(filePath, "utf-8");
8457
- data = JSON.parse(content);
8830
+ const data = JSON.parse(content);
8458
8831
  cache3.set(locale, data);
8459
8832
  return data;
8460
8833
  } catch {
8461
8834
  }
8462
- if (!data) {
8463
- const short = locale.split("-")[0];
8464
- if (short !== locale) {
8465
- const fallback = cache3.get(short) || await load(short);
8466
- if (fallback && Object.keys(fallback).length > 0) {
8467
- cache3.set(locale, fallback);
8468
- return fallback;
8469
- }
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;
8470
8841
  }
8471
8842
  }
8472
8843
  return {};
8473
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
+ }
8474
8856
  return async (req, ctx, next) => {
8475
8857
  const url = new URL(req.url);
8476
- const langMatch = url.pathname.match(/^\/__lang\/([\w-]+)$/);
8477
- if (langMatch && req.method === "GET") {
8478
- return handlePrefSwitch(req, langMatch[1], localeOpts.cookie, load);
8479
- }
8480
- const themeMatch = url.pathname.match(/^\/__theme\/([\w-]+)$/);
8481
- if (themeMatch && req.method === "GET") {
8482
- return handlePrefSwitch(req, themeMatch[1], themeOpts.cookie, load);
8483
- }
8484
- const locale = detectLocale(req, localeOpts);
8485
- const theme = detectTheme(req, themeOpts);
8486
- ctx.prefs = { locale, theme };
8487
- if (dir) {
8488
- const msgs = await load(locale);
8489
- ctx.t = (key, params, fallback) => translate(msgs, key, params, fallback);
8490
- globalThis.__LOCALE_DATA__ = msgs;
8491
- ctx.parsed = { ...ctx.parsed, __localeData: msgs };
8492
- }
8493
- ctx.setPref = (name, value) => {
8494
- 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
+ }
8495
8870
  const referer = req.headers.get("referer") || "/";
8496
- return new Response(null, {
8497
- status: 302,
8498
- headers: {
8499
- Location: referer,
8500
- "Set-Cookie": cookieOpts.join("; ")
8501
- }
8502
- });
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
+ }
8503
8884
  };
8504
- const flashVal = getCookies(req)["flash"] ?? null;
8505
- 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) {
8506
8910
  try {
8507
- ctx.prefs.flash = JSON.parse(flashVal);
8911
+ value = JSON.parse(decodeURIComponent(raw));
8508
8912
  } catch {
8509
- ctx.prefs.flash = flashVal;
8913
+ value = raw;
8510
8914
  }
8511
8915
  }
8916
+ ;
8917
+ ctx.flash = {
8918
+ value,
8919
+ set: makeSetFlash(name, referer)
8920
+ };
8512
8921
  const res = await next(req, ctx);
8513
- if (flashVal) {
8922
+ if (raw) {
8514
8923
  const headers = new Headers(res.headers);
8515
- headers.append("Set-Cookie", "flash=; Path=/; Max-Age=0");
8924
+ headers.append("Set-Cookie", `${name}=; Path=/; Max-Age=0`);
8516
8925
  return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
8517
8926
  }
8518
8927
  return res;
8519
8928
  };
8520
8929
  }
8521
- function detectLocale(req, opts) {
8522
- if (opts.cookie) {
8523
- const fromCookie = getCookies(req)[opts.cookie];
8524
- if (fromCookie) return fromCookie;
8525
- }
8526
- if (opts.fromAcceptLanguage) {
8527
- const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.trim();
8528
- if (fromHeader) return fromHeader;
8529
- }
8530
- return opts.default;
8531
- }
8532
- function detectTheme(req, opts) {
8533
- if (opts.cookie) {
8534
- const fromCookie = getCookies(req)[opts.cookie];
8535
- if (fromCookie) return fromCookie;
8536
- }
8537
- return opts.default;
8538
- }
8539
8930
 
8540
8931
  // seo.ts
8541
8932
  function escapeXml(s) {
@@ -10068,7 +10459,6 @@ function session(options) {
10068
10459
  }
10069
10460
  const snapshot = isSessionActive(session2) ? JSON.stringify(session2) : null;
10070
10461
  ctx.session = session2;
10071
- ctx.sessionId = session2.id;
10072
10462
  const res = await next(req, ctx);
10073
10463
  const currentSession = ctx.session;
10074
10464
  if (!currentSession || currentSession[kDestroyed]) {
@@ -10577,7 +10967,7 @@ function resolveTableName(table) {
10577
10967
  }
10578
10968
  return name;
10579
10969
  }
10580
- function escapeIdent(s) {
10970
+ function escapeIdent4(s) {
10581
10971
  return `"${s.replace(/"/g, '""')}"`;
10582
10972
  }
10583
10973
  function sqlLit(s) {
@@ -10587,11 +10977,11 @@ async function createIndex(sql2, table, fields, options) {
10587
10977
  const language = options?.language ?? "english";
10588
10978
  const tableName = resolveTableName(table);
10589
10979
  const indexName = options?.indexName ?? `${tableName}_fts_idx`;
10590
- const vectorExpr = fields.map((f) => `coalesce(${escapeIdent(f)}, '')`).join(` || ' ' || `);
10980
+ const vectorExpr = fields.map((f) => `coalesce(${escapeIdent4(f)}, '')`).join(` || ' ' || `);
10591
10981
  const indexType = options?.indexType ?? "gin";
10592
10982
  await sql2.unsafe(`
10593
- CREATE INDEX IF NOT EXISTS ${escapeIdent(indexName)}
10594
- ON ${escapeIdent(tableName)}
10983
+ CREATE INDEX IF NOT EXISTS ${escapeIdent4(indexName)}
10984
+ ON ${escapeIdent4(tableName)}
10595
10985
  USING ${indexType}
10596
10986
  (to_tsvector(${sqlLit(language)}, ${vectorExpr}))
10597
10987
  `);
@@ -10599,7 +10989,7 @@ async function createIndex(sql2, table, fields, options) {
10599
10989
  async function dropIndex(sql2, table, options) {
10600
10990
  const tableName = resolveTableName(table);
10601
10991
  const indexName = options?.indexName ?? `${tableName}_fts_idx`;
10602
- await sql2.unsafe(`DROP INDEX IF EXISTS ${escapeIdent(indexName)}`);
10992
+ await sql2.unsafe(`DROP INDEX IF EXISTS ${escapeIdent4(indexName)}`);
10603
10993
  }
10604
10994
  async function search(sql2, table, query, options) {
10605
10995
  const tableName = resolveTableName(table);
@@ -10613,13 +11003,13 @@ async function search(sql2, table, query, options) {
10613
11003
  }
10614
11004
  const sanitized = query.trim();
10615
11005
  if (!sanitized) return [];
10616
- const vectorExpr = searchFields.map((f) => `coalesce(${escapeIdent(f)}, '')`).join(` || ' ' || `);
11006
+ const vectorExpr = searchFields.map((f) => `coalesce(${escapeIdent4(f)}, '')`).join(` || ' ' || `);
10617
11007
  const langLit = sqlLit(language);
10618
11008
  const queryLit = sqlLit(sanitized);
10619
- const rankColId = escapeIdent(rankCol);
10620
- const tableId = escapeIdent(tableName);
11009
+ const rankColId = escapeIdent4(rankCol);
11010
+ const tableId = escapeIdent4(tableName);
10621
11011
  const headlineExpr = options?.headline ? searchFields.map(
10622
- (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")}`
10623
11013
  ).join(",\n ") : "";
10624
11014
  const sql_query = `
10625
11015
  SELECT
@@ -10659,8 +11049,8 @@ async function suggest(sql2, table, prefix, options) {
10659
11049
  const rows = await sql2.unsafe(`
10660
11050
  SELECT DISTINCT ts_lexize(${sqlLit(language)}, word) as tokens
10661
11051
  FROM (
10662
- SELECT regexp_split_to_table(lower(${escapeIdent(field)}), E'\\W+') as word
10663
- FROM ${escapeIdent(tableName)}
11052
+ SELECT regexp_split_to_table(lower(${escapeIdent4(field)}), E'\\W+') as word
11053
+ FROM ${escapeIdent4(tableName)}
10664
11054
  ) words
10665
11055
  WHERE word LIKE ${sqlLit(sanitized + "%")}
10666
11056
  LIMIT ${limit}
@@ -10782,75 +11172,45 @@ function s3(options) {
10782
11172
  return mw;
10783
11173
  }
10784
11174
 
10785
- // kb.ts
10786
- function chunkContent2(content, chunkSize, overlap) {
10787
- const paragraphs = content.split(/\n\n+/);
10788
- const chunks = [];
10789
- let current = "";
10790
- for (const p of paragraphs) {
10791
- if (current.length + p.length > chunkSize && current.length > 0) {
10792
- chunks.push(current);
10793
- current = current.slice(-overlap);
10794
- }
10795
- current += (current ? "\n\n" : "") + p;
10796
- }
10797
- if (current) chunks.push(current);
10798
- return chunks;
10799
- }
10800
- function escapeIdent2(s) {
11175
+ // kb/index.ts
11176
+ function escapeIdent5(s) {
10801
11177
  return `"${s.replace(/"/g, '""')}"`;
10802
11178
  }
10803
11179
  function knowledgeBase(options) {
10804
- const {
10805
- sql: sql2,
10806
- embedding: embedFn,
10807
- dimensions = 1536,
10808
- table = "_kb_docs",
10809
- chunkSize = 512,
10810
- chunkOverlap = 64,
10811
- searchLimit = 5,
10812
- searchThreshold = 0
10813
- } = 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
+ });
10814
11193
  async function migrate() {
10815
11194
  await sql2.unsafe(`CREATE EXTENSION IF NOT EXISTS "vector"`);
10816
- await sql2.unsafe(`
10817
- CREATE TABLE IF NOT EXISTS ${escapeIdent2(table)} (
10818
- id SERIAL PRIMARY KEY,
10819
- doc_key TEXT NOT NULL,
10820
- title TEXT NOT NULL DEFAULT '',
10821
- content TEXT NOT NULL,
10822
- chunk_index INTEGER NOT NULL DEFAULT 0,
10823
- metadata JSONB NOT NULL DEFAULT '{}',
10824
- embedding vector(${dimensions}),
10825
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
10826
- )
10827
- `);
10828
- await sql2.unsafe(`
10829
- CREATE INDEX IF NOT EXISTS ${escapeIdent2(table + "_key_idx")}
10830
- ON ${escapeIdent2(table)}(doc_key)
10831
- `);
10832
- await sql2.unsafe(`
10833
- CREATE INDEX IF NOT EXISTS ${escapeIdent2(table + "_embedding_idx")}
10834
- ON ${escapeIdent2(table)}
10835
- USING hnsw (embedding vector_cosine_ops)
10836
- `);
11195
+ await docsTable.create();
11196
+ await docsTable.createIndex("doc_key");
11197
+ await docsTable.createIndex("embedding", { type: "hnsw", operator: "vector_cosine_ops" });
10837
11198
  }
10838
11199
  async function ingest(key, content, ingestOpts) {
10839
- await sql2.unsafe(`DELETE FROM ${escapeIdent2(table)} WHERE doc_key = $1`, [key]);
10840
11200
  const title = ingestOpts?.title ?? key;
10841
11201
  const meta = ingestOpts?.metadata ?? {};
10842
11202
  const cs = ingestOpts?.chunkSize ?? chunkSize;
10843
11203
  const co = ingestOpts?.chunkOverlap ?? chunkOverlap;
10844
- const chunks = chunkContent2(content, cs, co);
11204
+ const chunks = chunkContent(content, cs, co);
10845
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)));
10846
11208
  for (let i = 0; i < chunks.length; i++) {
10847
- const chunk = chunks[i];
10848
- const embedding = await embedFn(chunk);
10849
- const vec = `[${embedding.join(",")}]`;
11209
+ const vec = `[${embeddings[i].join(",")}]`;
10850
11210
  await sql2.unsafe(
10851
- `INSERT INTO ${escapeIdent2(table)} (doc_key, title, content, chunk_index, metadata, embedding)
11211
+ `INSERT INTO ${escapeIdent5(table)} (doc_key, title, content, chunk_index, metadata, embedding)
10852
11212
  VALUES ($1, $2, $3, $4, $5::jsonb, $6::vector)`,
10853
- [key, title, chunk, i, metaJson, vec]
11213
+ [key, title, chunks[i], i, metaJson, vec]
10854
11214
  );
10855
11215
  }
10856
11216
  return chunks.length;
@@ -10858,13 +11218,13 @@ function knowledgeBase(options) {
10858
11218
  async function search2(query, searchOpts) {
10859
11219
  const limit = searchOpts?.limit ?? searchLimit;
10860
11220
  const threshold = searchOpts?.threshold ?? searchThreshold;
10861
- const embedding = await embedFn(query);
11221
+ const embedding = await provider.embed(query);
10862
11222
  const vec = `[${embedding.join(",")}]`;
10863
11223
  const whereClause = threshold > 0 ? `WHERE (1 - (embedding <=> $1::vector) / 2) >= ${threshold}` : "";
10864
11224
  const rows = await sql2.unsafe(
10865
11225
  `SELECT id, doc_key, title, content, chunk_index, metadata,
10866
11226
  1 - (embedding <=> $1::vector) / 2 AS _score
10867
- FROM ${escapeIdent2(table)}
11227
+ FROM ${escapeIdent5(table)}
10868
11228
  ${whereClause}
10869
11229
  ORDER BY embedding <=> $1::vector
10870
11230
  LIMIT ${limit}`,
@@ -10880,12 +11240,12 @@ function knowledgeBase(options) {
10880
11240
  }));
10881
11241
  }
10882
11242
  async function del(key) {
10883
- await sql2.unsafe(`DELETE FROM ${escapeIdent2(table)} WHERE doc_key = $1`, [key]);
11243
+ await sql2.unsafe(`DELETE FROM ${escapeIdent5(table)} WHERE doc_key = $1`, [key]);
10884
11244
  }
10885
11245
  async function list() {
10886
11246
  const rows = await sql2.unsafe(`
10887
11247
  SELECT doc_key, title, COUNT(*) AS chunks
10888
- FROM ${escapeIdent2(table)}
11248
+ FROM ${escapeIdent5(table)}
10889
11249
  GROUP BY doc_key, title
10890
11250
  ORDER BY doc_key
10891
11251
  `);
@@ -10911,6 +11271,158 @@ function knowledgeBase(options) {
10911
11271
  middleware: mw
10912
11272
  };
10913
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
+ }
10914
11426
  export {
10915
11427
  DEFAULT_MAX_BODY,
10916
11428
  MIGRATIONS_TABLE,
@@ -10923,6 +11435,7 @@ export {
10923
11435
  TestRequest,
10924
11436
  TsxContext,
10925
11437
  agent,
11438
+ aiProvider,
10926
11439
  aiStream,
10927
11440
  analytics,
10928
11441
  auth,
@@ -10930,7 +11443,7 @@ export {
10930
11443
  compress,
10931
11444
  cors,
10932
11445
  createHub,
10933
- createOpenAI,
11446
+ createOpenAI2 as createOpenAI,
10934
11447
  createSSEStream,
10935
11448
  createTestDb,
10936
11449
  createTestServer,
@@ -10943,6 +11456,7 @@ export {
10943
11456
  deploy,
10944
11457
  embed,
10945
11458
  embedMany,
11459
+ flash,
10946
11460
  formatSSE,
10947
11461
  formatSSEData,
10948
11462
  fts_exports as fts,
@@ -10952,6 +11466,7 @@ export {
10952
11466
  graphql,
10953
11467
  health,
10954
11468
  helmet,
11469
+ i18n,
10955
11470
  iii,
10956
11471
  isDev,
10957
11472
  isProd,
@@ -10961,11 +11476,10 @@ export {
10961
11476
  logger,
10962
11477
  mailer,
10963
11478
  messager,
10964
- oauthClient,
10965
11479
  openai,
10966
11480
  opencode,
11481
+ permissions,
10967
11482
  postgres,
10968
- preferences,
10969
11483
  queue,
10970
11484
  rateLimit,
10971
11485
  redis,
@@ -10987,6 +11501,7 @@ export {
10987
11501
  streamText,
10988
11502
  tenant,
10989
11503
  testApp,
11504
+ theme,
10990
11505
  tool2 as tool,
10991
11506
  traceElapsed,
10992
11507
  upload,