weifuwu 0.22.2 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ import type { KBOptions, KBModule, KBIngestOptions, KBSearchOptions, KBSearchResult, KBListEntry } from './types.ts';
2
+ export type { KBOptions, KBIngestOptions, KBSearchResult, KBSearchOptions, KBListEntry, KBModule };
3
+ export declare function knowledgeBase(options: KBOptions): KBModule;
@@ -0,0 +1,64 @@
1
+ import type { Middleware } from '../types.ts';
2
+ import type { AIProvider } from '../ai/provider.ts';
3
+ import type { PostgresClient } from '../postgres/types.ts';
4
+ export interface KBOptions {
5
+ /** Postgres client instance. */
6
+ pg: PostgresClient;
7
+ /** AI provider for embedding. */
8
+ provider: AIProvider;
9
+ /** Table name (default: '_kb_docs'). */
10
+ table?: string;
11
+ /** Default chunk size in characters (default: 512). */
12
+ chunkSize?: number;
13
+ /** Default chunk overlap in characters (default: 64). */
14
+ chunkOverlap?: number;
15
+ /** Default search limit (default: 5). */
16
+ searchLimit?: number;
17
+ /** Minimum similarity score threshold (0–1, default: 0). Set higher for stricter matches. */
18
+ searchThreshold?: number;
19
+ }
20
+ export interface KBIngestOptions {
21
+ title?: string;
22
+ metadata?: Record<string, unknown>;
23
+ chunkSize?: number;
24
+ chunkOverlap?: number;
25
+ }
26
+ export interface KBSearchResult {
27
+ id: number;
28
+ key: string;
29
+ title: string;
30
+ content: string;
31
+ score: number;
32
+ metadata: Record<string, unknown>;
33
+ }
34
+ export interface KBSearchOptions {
35
+ limit?: number;
36
+ /** Minimum cosine similarity score (0–1). Results below this are excluded. */
37
+ threshold?: number;
38
+ }
39
+ export interface KBListEntry {
40
+ key: string;
41
+ title: string;
42
+ chunks: number;
43
+ }
44
+ export interface KBModule {
45
+ /**
46
+ * Ingest a document: chunk → embed → store.
47
+ * If a document with the same key exists, it is replaced (delete + re-insert).
48
+ * Returns the number of chunks created.
49
+ */
50
+ ingest(key: string, content: string, options?: KBIngestOptions): Promise<number>;
51
+ /**
52
+ * Search the knowledge base by semantic similarity.
53
+ * Query is embedded, then vector similarity search returns top results.
54
+ */
55
+ search(query: string, searchOptions?: KBSearchOptions): Promise<KBSearchResult[]>;
56
+ /** Delete all chunks for a document key. */
57
+ delete(key: string): Promise<void>;
58
+ /** List all document keys with title and chunk count. */
59
+ list(): Promise<KBListEntry[]>;
60
+ /** Create the table and HNSW index. Safe to call multiple times. */
61
+ migrate(): Promise<void>;
62
+ /** Middleware that injects `ctx.kb` with `.search()` method. */
63
+ middleware(): Middleware;
64
+ }
package/dist/kb.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ import type { Sql } from './vendor.ts';
2
+ import type { Middleware } from './types.ts';
3
+ export interface KBOptions {
4
+ /** Postgres SQL client (with pgvector extension enabled). */
5
+ sql: Sql<{}>;
6
+ /**
7
+ * Embedding function.
8
+ * Takes a text string, returns a vector of numbers.
9
+ * Example: (text) => embed({ model: openai.embedding('text-embedding-3-small'), value: text }).then(r => r.embedding)
10
+ */
11
+ embedding: (text: string) => Promise<number[]>;
12
+ /** Vector dimensions (default: 1536 for text-embedding-3-small). */
13
+ dimensions?: number;
14
+ /** Table name (default: '_kb_docs'). */
15
+ table?: string;
16
+ /** Default chunk size in characters (default: 512). */
17
+ chunkSize?: number;
18
+ /** Default chunk overlap in characters (default: 64). */
19
+ chunkOverlap?: number;
20
+ /** Default search limit (default: 5). */
21
+ searchLimit?: number;
22
+ /** Minimum similarity score threshold (0–1, default: 0). Set higher for stricter matches. */
23
+ searchThreshold?: number;
24
+ }
25
+ export interface KBIngestOptions {
26
+ title?: string;
27
+ metadata?: Record<string, unknown>;
28
+ chunkSize?: number;
29
+ chunkOverlap?: number;
30
+ }
31
+ export interface KBSearchResult {
32
+ id: number;
33
+ key: string;
34
+ title: string;
35
+ content: string;
36
+ score: number;
37
+ metadata: Record<string, unknown>;
38
+ }
39
+ export interface KBSearchOptions {
40
+ limit?: number;
41
+ /** Minimum cosine similarity score (0–1). Results below this are excluded. */
42
+ threshold?: number;
43
+ }
44
+ export interface KBListEntry {
45
+ key: string;
46
+ title: string;
47
+ chunks: number;
48
+ }
49
+ export interface KBModule {
50
+ /**
51
+ * Ingest a document: chunk → embed → store.
52
+ * If a document with the same key exists, it is replaced (delete + re-insert).
53
+ * Returns the number of chunks created.
54
+ */
55
+ ingest(key: string, content: string, options?: KBIngestOptions): Promise<number>;
56
+ /**
57
+ * Search the knowledge base by semantic similarity.
58
+ * Query is embedded, then vector similarity search returns top results.
59
+ */
60
+ search(query: string, searchOptions?: KBSearchOptions): Promise<KBSearchResult[]>;
61
+ /** Delete all chunks for a document key. */
62
+ delete(key: string): Promise<void>;
63
+ /** List all document keys with title and chunk count. */
64
+ list(): Promise<KBListEntry[]>;
65
+ /** Create the table and HNSW index. Safe to call multiple times. */
66
+ migrate(): Promise<void>;
67
+ /** Middleware that injects `ctx.kb` with `.search()` method. */
68
+ middleware(): Middleware;
69
+ }
70
+ export declare function knowledgeBase(options: KBOptions): KBModule;
@@ -0,0 +1,41 @@
1
+ import type { PostgresClient } from './postgres/types.ts';
2
+ import { Router } from './router.ts';
3
+ export interface OAuthProviderConfig {
4
+ clientId: string;
5
+ clientSecret: string;
6
+ scope?: string;
7
+ /** Custom auth URL (overrides built-in provider default). */
8
+ authUrl?: string;
9
+ /** Custom token URL (overrides built-in provider default). */
10
+ tokenUrl?: string;
11
+ /** Custom user info URL (overrides built-in provider default). */
12
+ userUrl?: string;
13
+ /**
14
+ * Custom user parser.
15
+ * Required when any of authUrl/tokenUrl/userUrl is custom.
16
+ * Receives the raw response from userUrl + the access token.
17
+ */
18
+ parseUser?: (data: any, accessToken: string) => ProviderUser;
19
+ }
20
+ export interface OAuthClientOptions {
21
+ /** Postgres client (required). */
22
+ pg: PostgresClient;
23
+ /** JWT secret — must match user() module's jwtSecret. */
24
+ jwtSecret: string;
25
+ /** JWT expiry (default: '24h'). */
26
+ expiresIn?: string | number;
27
+ /** Where to redirect after successful login (default: '/'). */
28
+ redirectUrl?: string;
29
+ /** Provider configurations. */
30
+ providers: Record<string, OAuthProviderConfig>;
31
+ /** Table name for provider-user links (default: '_auth_providers'). */
32
+ table?: string;
33
+ }
34
+ interface ProviderUser {
35
+ id: string;
36
+ email: string;
37
+ name: string;
38
+ avatarUrl?: string;
39
+ }
40
+ export declare function oauthClient(options: OAuthClientOptions): Router;
41
+ export {};
@@ -0,0 +1,49 @@
1
+ import type { Sql } from './vendor.ts';
2
+ import type { Middleware, Context } from './types.ts';
3
+ declare module './types.ts' {
4
+ interface Context {
5
+ roles: Set<string>;
6
+ permissions: Set<string>;
7
+ }
8
+ }
9
+ export interface PermissionsOptions {
10
+ /** PostgreSQL client. */
11
+ pg: {
12
+ sql: Sql<{}>;
13
+ };
14
+ /** Table prefix (default: '' → _roles, _user_roles, _role_permissions). */
15
+ prefix?: string;
16
+ }
17
+ export interface PermissionsModule extends Middleware {
18
+ /**
19
+ * Middleware that injects `ctx.permissions = { roles, permissions }`.
20
+ * Reads `ctx.user.id` to look up role assignments.
21
+ * Must be placed after a middleware that sets `ctx.user`.
22
+ */
23
+ (req: Request, ctx: Context, next: Handler): Response | Promise<Response>;
24
+ /** Assign a role to a user. Creates the role if it doesn't exist. */
25
+ assignRole(userId: number, role: string): Promise<void>;
26
+ /** Remove a role from a user. */
27
+ removeRole(userId: number, role: string): Promise<void>;
28
+ /** Grant a permission to a role. Creates the role if it doesn't exist. */
29
+ grantPermission(role: string, permission: string): Promise<void>;
30
+ /** Revoke a permission from a role. */
31
+ revokePermission(role: string, permission: string): Promise<void>;
32
+ /** Get all roles assigned to a user. */
33
+ getUserRoles(userId: number): Promise<string[]>;
34
+ /** Get all permissions for a user (union of all role permissions). */
35
+ getUserPermissions(userId: number): Promise<string[]>;
36
+ /**
37
+ * Middleware that rejects the request if the user does not have any of the specified roles.
38
+ * Must be placed after `permissions()` middleware (which injects ctx.permissions.roles).
39
+ */
40
+ requireRole(...roles: string[]): Middleware;
41
+ /**
42
+ * Middleware that rejects the request if the user does not have all specified permissions.
43
+ * Must be placed after `permissions()` middleware (which injects ctx.permissions.permissions).
44
+ */
45
+ requirePermission(...permissions: string[]): Middleware;
46
+ /** Create the underlying tables. Safe to call multiple times. */
47
+ migrate(): Promise<void>;
48
+ }
49
+ export declare function permissions(options: PermissionsOptions): PermissionsModule;
@@ -14,10 +14,16 @@ export interface QueueJob<T = unknown> {
14
14
  schedule?: string;
15
15
  }
16
16
  export interface QueueOptions {
17
+ /** Backend store. Default: 'memory'. */
18
+ store?: 'memory' | 'pg' | 'redis';
17
19
  redis?: Redis;
18
20
  url?: string;
19
21
  prefix?: string;
20
22
  pollInterval?: number;
23
+ /** PostgreSQL client (required when store: 'pg'). */
24
+ pg?: {
25
+ sql: import('../vendor.ts').Sql<{}>;
26
+ };
21
27
  }
22
28
  export interface QueueInjected {
23
29
  queue: Queue;
@@ -27,6 +33,10 @@ export interface QueueJobWithError<T = unknown> extends QueueJob<T> {
27
33
  failedAt: number;
28
34
  }
29
35
  export interface Queue extends Middleware<Context, Context & QueueInjected> {
36
+ /** Register a cron job. Uses queue's backend (memory/pg/redis) for execution. */
37
+ cron(pattern: string, handler: () => void | Promise<void>): {
38
+ stop: () => void;
39
+ };
30
40
  add<T>(type: string, payload: T, opts?: {
31
41
  delay?: number;
32
42
  schedule?: string;
@@ -34,7 +44,6 @@ export interface Queue extends Middleware<Context, Context & QueueInjected> {
34
44
  process<T>(type: string, handler: (job: QueueJob<T>) => Promise<void>): void;
35
45
  run(): Promise<void>;
36
46
  stop(): void;
37
- /** Stats: { running, inflight, processed, failed, handlers, maxConcurrent } */
38
47
  stats(): {
39
48
  running: boolean;
40
49
  inflight: number;
@@ -43,15 +52,12 @@ export interface Queue extends Middleware<Context, Context & QueueInjected> {
43
52
  handlers: number;
44
53
  maxConcurrent: number;
45
54
  };
46
- /** List pending jobs (up to `limit`). */
47
55
  jobs(limit?: number): Promise<QueueJob[]>;
48
- /** List failed jobs (up to `limit`). */
49
56
  failedJobs(limit?: number): Promise<QueueJobWithError[]>;
50
- /** Retry a specific failed job by re-adding it to the queue. */
51
57
  retryFailed(jobId: string): Promise<boolean>;
52
- /** Retry all failed jobs matching a type (or all types if omitted). */
53
58
  retryAllFailed(type?: string): Promise<number>;
54
- /** Returns a Router with management dashboard endpoints (GET/POST). */
55
59
  dashboard(): import('../router.ts').Router;
60
+ /** Create the jobs table (PG mode only; safe to call multiple times). */
61
+ migrate?(): Promise<void>;
56
62
  close(): Promise<void>;
57
63
  }
package/dist/react.d.ts CHANGED
@@ -3,7 +3,7 @@ export type { UseWebsocketOptions, UseWebsocketReturn } from './use-websocket.ts
3
3
  export { useAction } from './use-action.ts';
4
4
  export type { UseActionOptions, UseActionReturn } from './use-action.ts';
5
5
  export { Link, useNavigate, navigate, useNavigating, addInterceptor } from './client-router.ts';
6
- export { TsxContext, useLoaderData } from './tsx-context.ts';
6
+ export { TsxContext, useCtx, setCtx, addCtxRebuilder, useLoaderData } from './tsx-context.ts';
7
7
  export { Head } from './head.tsx';
8
8
  export { createStore, useFetch, useQueryState } from './client-state.ts';
9
9
  export type { StoreApi } from './client-state.ts';
package/dist/react.js CHANGED
@@ -153,6 +153,65 @@ async function runInterceptors(url) {
153
153
  return false;
154
154
  }
155
155
 
156
+ // tsx-context.ts
157
+ import { useSyncExternalStore, createContext } from "react";
158
+ var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, loaderData: {}, env: {}, user: {} };
159
+ var KEY = "__WEIFUWU_CTX_STORE";
160
+ function getStore() {
161
+ if (typeof globalThis !== "undefined" && globalThis[KEY]) {
162
+ return globalThis[KEY];
163
+ }
164
+ const s = {
165
+ _ctx: DEFAULT_CTX,
166
+ _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 },
167
+ _listeners: /* @__PURE__ */ new Set(),
168
+ _rebuilders: [],
169
+ _alsGetStore: null
170
+ };
171
+ if (typeof globalThis !== "undefined") {
172
+ globalThis[KEY] = s;
173
+ }
174
+ return s;
175
+ }
176
+ var store = getStore();
177
+ function addCtxRebuilder(fn) {
178
+ store._rebuilders.push(fn);
179
+ }
180
+ var subscribe = (cb) => {
181
+ store._listeners.add(cb);
182
+ return () => {
183
+ store._listeners.delete(cb);
184
+ };
185
+ };
186
+ var getSnapshot = () => store._snapshot;
187
+ function setCtx(value) {
188
+ if (typeof window !== "undefined") {
189
+ for (const r of store._rebuilders) {
190
+ const rebuilt = r(value);
191
+ if (rebuilt) Object.assign(value, rebuilt);
192
+ }
193
+ }
194
+ store._ctx = { ...store._ctx, ...value };
195
+ 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 };
196
+ if (typeof window !== "undefined") {
197
+ ;
198
+ window.__WEIFUWU_CTX = { ...window.__WEIFUWU_CTX, ...value };
199
+ }
200
+ store._listeners.forEach((fn) => fn());
201
+ }
202
+ function useCtx() {
203
+ if (typeof window !== "undefined") {
204
+ return useSyncExternalStore(subscribe, getSnapshot);
205
+ }
206
+ const alsStore = store._alsGetStore?.();
207
+ return alsStore ?? store._ctx;
208
+ }
209
+ function useLoaderData() {
210
+ const ctx = useCtx();
211
+ return ctx.loaderData;
212
+ }
213
+ var TsxContext = createContext(DEFAULT_CTX);
214
+
156
215
  // client-router.ts
157
216
  var _navigating = false;
158
217
  var _listeners = [];
@@ -187,37 +246,25 @@ async function navigate(href) {
187
246
  return;
188
247
  }
189
248
  const newHtml = rootEl.innerHTML;
190
- const propsMatch = html.match(/window\.__WEIFUWU_PROPS=(.+?)<\/script>/);
191
- if (!propsMatch) {
192
- location.href = href;
193
- return;
194
- }
195
249
  const bundleMatch = html.match(/src="(\/__ssr\/[^"]+\.js)"/);
196
250
  const bundleUrl = bundleMatch ? bundleMatch[1] : null;
251
+ const ctxMatch = html.match(/window\.__WEIFUWU_CTX=(.+?)<\/script>/);
252
+ if (ctxMatch) {
253
+ try {
254
+ const ctx = JSON.parse(ctxMatch[1]);
255
+ window.__WEIFUWU_CTX = ctx;
256
+ setCtx(ctx);
257
+ } catch {
258
+ }
259
+ }
197
260
  applyHead(html);
198
261
  const currentRoot = document.getElementById("__weifuwu_root");
199
262
  if (!currentRoot) {
200
263
  location.href = href;
201
264
  return;
202
265
  }
203
- ;
204
- window.__WEIFUWU_PROPS = JSON.parse(propsMatch[1]);
205
266
  history.pushState(null, "", url.pathname + url.search);
206
267
  currentRoot.innerHTML = newHtml;
207
- const ctxMatch = html.match(/window\.__WEIFUWU_CTX=(.+?)<\/script>/);
208
- if (ctxMatch) {
209
- try {
210
- window.__WEIFUWU_CTX = JSON.parse(ctxMatch[1]);
211
- } catch {
212
- }
213
- }
214
- const localeMatch = html.match(/window\.__LOCALE_DATA__=(.+?)<\/script>/);
215
- if (localeMatch) {
216
- try {
217
- window.__LOCALE_DATA__ = JSON.parse(localeMatch[1]);
218
- } catch {
219
- }
220
- }
221
268
  if (bundleUrl) {
222
269
  try {
223
270
  await import(
@@ -330,52 +377,6 @@ async function prefetchPage(href) {
330
377
  }
331
378
  }
332
379
 
333
- // tsx-context.ts
334
- import { useSyncExternalStore, createContext } from "react";
335
- var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, loaderData: {}, env: {}, user: {} };
336
- var KEY = "__WEIFUWU_CTX_STORE";
337
- function getStore() {
338
- if (typeof globalThis !== "undefined" && globalThis[KEY]) {
339
- return globalThis[KEY];
340
- }
341
- const s = {
342
- _ctx: DEFAULT_CTX,
343
- _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 },
344
- _listeners: /* @__PURE__ */ new Set(),
345
- _alsGetStore: null
346
- };
347
- if (typeof globalThis !== "undefined") {
348
- globalThis[KEY] = s;
349
- }
350
- return s;
351
- }
352
- var store = getStore();
353
- var subscribe = (cb) => {
354
- store._listeners.add(cb);
355
- return () => {
356
- store._listeners.delete(cb);
357
- };
358
- };
359
- var getSnapshot = () => store._snapshot;
360
- function setCtx(value) {
361
- store._ctx = { ...store._ctx, ...value };
362
- 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 };
363
- store._listeners.forEach((fn) => fn());
364
- }
365
- function useCtx() {
366
- if (typeof window !== "undefined") {
367
- const snapshot = useSyncExternalStore(subscribe, getSnapshot);
368
- return { ...snapshot, ...window.__WEIFUWU_CTX };
369
- }
370
- const alsStore = store._alsGetStore?.();
371
- return alsStore ?? store._ctx;
372
- }
373
- function useLoaderData() {
374
- const ctx = useCtx();
375
- return ctx.loaderData;
376
- }
377
- var TsxContext = createContext(DEFAULT_CTX);
378
-
379
380
  // head.tsx
380
381
  import { createElement as createElement2 } from "react";
381
382
  function Head({ children }) {
@@ -516,9 +517,10 @@ function useQueryState(key, defaultValue = "") {
516
517
  }
517
518
 
518
519
  // client-locale.ts
519
- function buildT() {
520
- const messages = globalThis.__LOCALE_DATA__ || (typeof window !== "undefined" ? window.__LOCALE_DATA__ : null);
521
- if (!messages) return (key, _p, fb) => fb ?? key;
520
+ function buildT(messages) {
521
+ if (!messages || Object.keys(messages).length === 0) {
522
+ return (key, _p, fb) => fb ?? key;
523
+ }
522
524
  return (key, params, fallback) => {
523
525
  const msg = key.split(".").reduce((o, k) => o?.[k], messages);
524
526
  if (msg === void 0 || msg === null) return fallback ?? key;
@@ -528,6 +530,12 @@ function buildT() {
528
530
  return result;
529
531
  };
530
532
  }
533
+ addCtxRebuilder((value) => {
534
+ if (value.i18n?.messages) {
535
+ return { i18n: { ...value.i18n, t: buildT(value.i18n.messages) } };
536
+ }
537
+ return null;
538
+ });
531
539
  addInterceptor(async (url) => {
532
540
  const m = url.pathname.match(/^\/__lang\/([\w-]+)$/);
533
541
  if (!m) return false;
@@ -536,11 +544,7 @@ addInterceptor(async (url) => {
536
544
  headers: { accept: "application/json" }
537
545
  });
538
546
  const data = await res.json();
539
- const ctx = { ...window.__WEIFUWU_CTX || {}, params: {}, query: {} };
540
- ctx.prefs = { ...ctx.prefs, locale: data.locale };
541
- if (data.messages) window.__LOCALE_DATA__ = data.messages;
542
- window.__WEIFUWU_CTX = ctx;
543
- setCtx(ctx);
547
+ setCtx({ i18n: { locale: data.locale, messages: data.messages || {} } });
544
548
  } catch {
545
549
  location.href = url.href;
546
550
  }
@@ -549,9 +553,9 @@ addInterceptor(async (url) => {
549
553
  function useLocale() {
550
554
  const ctx = useCtx();
551
555
  return {
552
- locale: ctx.prefs.locale,
556
+ locale: ctx.i18n?.locale,
553
557
  setLocale: (locale) => navigate("/__lang/" + locale),
554
- t: buildT()
558
+ t: ctx.i18n?.t ?? ((key, _p, fb) => fb ?? key)
555
559
  };
556
560
  }
557
561
 
@@ -573,7 +577,7 @@ function applyTheme(theme) {
573
577
  if (!_mqListener) {
574
578
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
575
579
  mq.addEventListener("change", (e) => {
576
- if (window.__WEIFUWU_CTX?.prefs?.theme === "system") {
580
+ if (window.__WEIFUWU_CTX?.theme?.value === "system") {
577
581
  document.documentElement.dataset.theme = e.matches ? "dark" : "light";
578
582
  }
579
583
  });
@@ -589,11 +593,9 @@ addInterceptor(async (url) => {
589
593
  headers: { accept: "application/json" }
590
594
  });
591
595
  const data = await res.json();
592
- const ctx = { ...window.__WEIFUWU_CTX || {}, params: {}, query: {} };
593
- ctx.prefs = { ...ctx.prefs, theme: data.theme };
594
- window.__WEIFUWU_CTX = ctx;
596
+ window.__WEIFUWU_CTX = { ...window.__WEIFUWU_CTX, theme: { value: data.theme } };
597
+ setCtx({ theme: { value: data.theme } });
595
598
  applyTheme(data.theme);
596
- setCtx(ctx);
597
599
  } catch {
598
600
  location.href = url.href;
599
601
  }
@@ -601,7 +603,7 @@ addInterceptor(async (url) => {
601
603
  });
602
604
  function useTheme() {
603
605
  const ctx = useCtx();
604
- const theme = ctx.prefs.theme ?? "system";
606
+ const theme = ctx.theme?.value ?? "system";
605
607
  useEffect4(() => {
606
608
  applyTheme(theme);
607
609
  }, [theme]);
@@ -617,9 +619,9 @@ import { useState as useState5 } from "react";
617
619
  function useFlashMessage() {
618
620
  const [flash] = useState5(() => {
619
621
  if (typeof window === "undefined") return null;
620
- const raw = window.__WEIFUWU_CTX?.prefs?.flash;
621
- if (!raw) return null;
622
- return typeof raw === "string" ? JSON.parse(raw) : raw;
622
+ const raw = window.__WEIFUWU_CTX?.flash?.value;
623
+ if (raw === void 0 || raw === null) return null;
624
+ return raw;
623
625
  });
624
626
  return flash;
625
627
  }
@@ -683,12 +685,15 @@ export {
683
685
  Head,
684
686
  Link,
685
687
  TsxContext,
688
+ addCtxRebuilder,
686
689
  addInterceptor,
687
690
  applyTheme,
688
691
  createStore,
689
692
  navigate,
693
+ setCtx,
690
694
  useAction,
691
695
  useAgentStream,
696
+ useCtx,
692
697
  useFetch,
693
698
  useFlashMessage,
694
699
  useLoaderData,
package/dist/s3.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
2
+ import type { Middleware } from './types.ts';
3
+ import type { Readable } from 'node:stream';
4
+ declare module './types.ts' {
5
+ interface Context {
6
+ s3: S3Module;
7
+ }
8
+ }
9
+ export interface S3Options {
10
+ /** S3 bucket name. Required. */
11
+ bucket: string;
12
+ /** AWS region. Default: 'us-east-1'. */
13
+ region?: string;
14
+ /** Custom endpoint (MinIO, Cloudflare R2, Backblaze B2, etc.). */
15
+ endpoint?: string;
16
+ /** Force path-style addressing (required for MinIO, some private clouds). */
17
+ forcePathStyle?: boolean;
18
+ /** AWS credentials. Falls back to AWS env vars / IAM role if omitted. */
19
+ credentials?: {
20
+ accessKeyId: string;
21
+ secretAccessKey: string;
22
+ };
23
+ /**
24
+ * Base public URL for unsigned URLs.
25
+ * When set, `s3.url(key, { expiresIn: 0 })` returns a public URL.
26
+ * Example: 'https://cdn.example.com' or 'https://pub-xxx.r2.dev'
27
+ */
28
+ publicUrl?: string;
29
+ }
30
+ export interface S3PutOptions {
31
+ /** Content-Type of the object. */
32
+ contentType?: string;
33
+ /** Cache-Control header value. */
34
+ cacheControl?: string;
35
+ /** User-defined metadata (prefix x-amz-meta-). */
36
+ metadata?: Record<string, string>;
37
+ }
38
+ export interface S3UrlOptions {
39
+ /**
40
+ * Signed URL expiry in seconds.
41
+ * Default: 3600 (1 hour).
42
+ * Set to 0 to return an unsigned public URL (requires `publicUrl` option).
43
+ */
44
+ expiresIn?: number;
45
+ }
46
+ export interface S3Module {
47
+ /** Upload a file. Returns the key. */
48
+ put(key: string, body: S3Body, options?: S3PutOptions): Promise<string>;
49
+ /** Download a file. Returns the body as Buffer, or null if not found. */
50
+ get(key: string): Promise<Buffer | null>;
51
+ /** Delete a file. */
52
+ delete(key: string): Promise<void>;
53
+ /** Check if a file exists. */
54
+ exists(key: string): Promise<boolean>;
55
+ /**
56
+ * Generate a URL for an object.
57
+ * - If `expiresIn` > 0 (default): returns a signed URL with that expiry.
58
+ * - If `expiresIn` === 0 and `publicUrl` is configured: returns an unsigned
59
+ * public URL. Throws if `publicUrl` is not set.
60
+ */
61
+ url(key: string, options?: S3UrlOptions): Promise<string>;
62
+ /** List object keys under a prefix. */
63
+ list(prefix?: string): Promise<string[]>;
64
+ /** The underlying S3Client (for advanced usage). */
65
+ client: S3Client;
66
+ }
67
+ export type S3Body = Buffer | Uint8Array | string | ReadableStream | Readable;
68
+ export declare function s3(options: S3Options): S3Module & Middleware;
package/dist/serve.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface ServeOptions {
17
17
  shutdown?: boolean;
18
18
  }
19
19
  export interface Server {
20
- stop: () => Promise<void>;
20
+ stop: (timeoutMs?: number) => Promise<void>;
21
21
  readonly port: number;
22
22
  readonly hostname: string;
23
23
  ready: Promise<void>;
package/dist/session.d.ts CHANGED
@@ -3,7 +3,6 @@ import type { Redis } from './vendor.ts';
3
3
  declare module './types.ts' {
4
4
  interface Context {
5
5
  session: Session;
6
- sessionId: string;
7
6
  }
8
7
  }
9
8
  declare const kSaved: unique symbol;
@@ -50,6 +49,18 @@ export interface SessionOptions {
50
49
  secure?: boolean;
51
50
  sameSite?: 'strict' | 'lax' | 'none';
52
51
  };
52
+ /**
53
+ * Secret for signing the session cookie with HMAC-SHA256.
54
+ * When set, the cookie value becomes `sid.signature` — tampering is detected
55
+ * and rejected. Strongly recommended in production.
56
+ */
57
+ secret?: string;
58
+ /**
59
+ * Interval (ms) for automatic session ID rotation.
60
+ * Rotating the ID mitigates session fixation attacks.
61
+ * Default: 900_000 (15 min). Set to 0 to disable.
62
+ */
63
+ rotateInterval?: number;
53
64
  }
54
65
  export interface SessionInjected {
55
66
  session: Session;
package/dist/ssr.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Router } from './router.ts';
2
- export declare function markClientBundleDirty(): void;
3
2
  interface ResolvedRoute {
4
3
  routePath: string;
5
4
  pageFile: string;