silgi 0.51.6 → 0.51.8

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.
Files changed (53) hide show
  1. package/README.md +47 -0
  2. package/dist/adapters/_fetch-adapter.d.mts +6 -0
  3. package/dist/adapters/_fetch-adapter.mjs +14 -8
  4. package/dist/adapters/astro.mjs +1 -1
  5. package/dist/adapters/nextjs.mjs +1 -1
  6. package/dist/adapters/remix.mjs +1 -1
  7. package/dist/adapters/solidstart.mjs +1 -1
  8. package/dist/adapters/sveltekit.mjs +1 -1
  9. package/dist/client/client.d.mts +42 -4
  10. package/dist/client/client.mjs +42 -4
  11. package/dist/client/server.d.mts +27 -2
  12. package/dist/client/server.mjs +27 -2
  13. package/dist/compile.d.mts +10 -1
  14. package/dist/compile.mjs +13 -4
  15. package/dist/core/context-bridge.d.mts +49 -0
  16. package/dist/core/context-bridge.mjs +43 -7
  17. package/dist/core/context.d.mts +26 -0
  18. package/dist/core/ctx-symbols.mjs +21 -0
  19. package/dist/core/error.d.mts +183 -2
  20. package/dist/core/error.mjs +259 -16
  21. package/dist/core/handler.d.mts +15 -1
  22. package/dist/core/handler.mjs +33 -17
  23. package/dist/core/schema-converter.d.mts +131 -0
  24. package/dist/core/schema-converter.mjs +82 -0
  25. package/dist/core/serve.d.mts +2 -2
  26. package/dist/core/serve.mjs +9 -2
  27. package/dist/core/task.mjs +2 -2
  28. package/dist/index.d.mts +5 -2
  29. package/dist/index.mjs +4 -2
  30. package/dist/integrations/better-auth/index.d.mts +22 -1
  31. package/dist/integrations/better-auth/index.mjs +79 -11
  32. package/dist/integrations/drizzle/index.mjs +22 -5
  33. package/dist/integrations/zod/converter.d.mts +1 -1
  34. package/dist/integrations/zod/index.d.mts +29 -2
  35. package/dist/integrations/zod/index.mjs +60 -1
  36. package/dist/lazy.d.mts +40 -3
  37. package/dist/lazy.mjs +40 -3
  38. package/dist/map-input.mjs +1 -1
  39. package/dist/plugins/analytics/collector.d.mts +1 -1
  40. package/dist/plugins/analytics/trace.mjs +1 -1
  41. package/dist/plugins/analytics/types.d.mts +3 -3
  42. package/dist/plugins/analytics/utils.mjs +1 -4
  43. package/dist/plugins/analytics.d.mts +5 -3
  44. package/dist/plugins/analytics.mjs +16 -29
  45. package/dist/plugins/cache.mjs +1 -1
  46. package/dist/plugins/coerce.mjs +1 -1
  47. package/dist/scalar.d.mts +2 -1
  48. package/dist/scalar.mjs +9 -30
  49. package/dist/silgi.d.mts +165 -18
  50. package/dist/silgi.mjs +50 -12
  51. package/package.json +7 -3
  52. package/dist/core/trace-map.d.mts +0 -13
  53. package/dist/core/trace-map.mjs +0 -13
@@ -1,11 +1,9 @@
1
1
  import { routerCache } from "./router-utils.mjs";
2
- import { compileRouter, createContext, releaseContext } from "../compile.mjs";
2
+ import { compileRouter, createContext, detachContext, releaseContext } from "../compile.mjs";
3
3
  import { applyContext } from "./dispatch.mjs";
4
4
  import { detectResponseFormat, encodeResponse, makeErrorResponse } from "./codec.mjs";
5
- import { runWithCtx } from "./context-bridge.mjs";
6
5
  import { parseInput } from "./input.mjs";
7
6
  import { iteratorToEventStream } from "./sse.mjs";
8
- import { analyticsTraceMap } from "./trace-map.mjs";
9
7
  import { parseUrlPath } from "./url.mjs";
10
8
  //#region src/core/handler.ts
11
9
  /**
@@ -46,20 +44,25 @@ function wrapStreamWithRelease(source, ctx) {
46
44
  }
47
45
  });
48
46
  }
47
+ /**
48
+ * Build the Response for a handler's output. The pooled `ctx` is released
49
+ * by the caller's `using` scope; for streaming outputs we detach `ctx` first
50
+ * so the stream becomes the sole owner (releases on stream end/cancel).
51
+ */
49
52
  function makeResponse(output, route, format, ctx) {
50
- if (output instanceof Response) {
51
- releaseContext(ctx);
52
- return output;
53
+ if (output instanceof Response) return output;
54
+ if (output instanceof ReadableStream) {
55
+ detachContext(ctx);
56
+ return new Response(wrapStreamWithRelease(output, ctx), { headers: { "content-type": "application/octet-stream" } });
53
57
  }
54
- if (output instanceof ReadableStream) return new Response(wrapStreamWithRelease(output, ctx), { headers: { "content-type": "application/octet-stream" } });
55
58
  if (output && typeof output === "object" && Symbol.asyncIterator in output) {
59
+ detachContext(ctx);
56
60
  const stream = iteratorToEventStream(output);
57
61
  return new Response(wrapStreamWithRelease(stream, ctx), { headers: {
58
62
  "content-type": "text/event-stream",
59
63
  "cache-control": "no-cache"
60
64
  } });
61
65
  }
62
- releaseContext(ctx);
63
66
  const cacheHeaders = route.cacheControl ? { "cache-control": route.cacheControl } : void 0;
64
67
  if (format !== "json") return encodeResponse(output, 200, format, cacheHeaders);
65
68
  return new Response(JSON.stringify(output), { headers: cacheHeaders ? {
@@ -81,12 +84,11 @@ function wrapHandler(handler, router, options, prefix) {
81
84
  if (options.scalar) {
82
85
  const { wrapWithScalar } = await import("../scalar.mjs");
83
86
  const scalarOpts = typeof options.scalar === "object" ? options.scalar : {};
84
- h = wrapWithScalar(h, router, scalarOpts, prefix);
87
+ h = wrapWithScalar(h, router, scalarOpts, prefix, options.schemaRegistry);
85
88
  }
86
89
  if (options.analytics) {
87
90
  const { wrapWithAnalytics } = await import("../plugins/analytics.mjs");
88
- const analyticsOpts = typeof options.analytics === "object" ? options.analytics : {};
89
- h = wrapWithAnalytics(h, router, analyticsOpts);
91
+ h = wrapWithAnalytics(h, router, options.analytics, options.schemaRegistry, options.hooks);
90
92
  }
91
93
  wrapped = h;
92
94
  }
@@ -96,7 +98,7 @@ function wrapHandler(handler, router, options, prefix) {
96
98
  return initPromise.then(() => wrapped(request));
97
99
  };
98
100
  }
99
- function createFetchHandler(routerDef, contextFactory, hooks, prefix) {
101
+ function createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge) {
100
102
  let compiledRouter = routerCache.get(routerDef);
101
103
  if (!compiledRouter) {
102
104
  compiledRouter = compileRouter(routerDef);
@@ -116,6 +118,13 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix) {
116
118
  if (result instanceof Promise) result.catch(() => {});
117
119
  } catch {}
118
120
  }
121
+ function awaitHook(name, event) {
122
+ if (!hooks) return;
123
+ try {
124
+ const result = hooks.callHook(name, event);
125
+ if (result instanceof Promise) return result.catch(() => {});
126
+ } catch {}
127
+ }
119
128
  return async function handleRequest(request) {
120
129
  const url = request.url;
121
130
  let fullPath = parseUrlPath(url);
@@ -149,14 +158,17 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix) {
149
158
  });
150
159
  }
151
160
  const format = detectResponseFormat(request);
152
- const ctx = createContext();
161
+ using ctx = createContext();
153
162
  let rawInput;
154
163
  try {
155
164
  const baseCtxResult = contextFactory(request);
156
165
  applyContext(ctx, baseCtxResult instanceof Promise ? await baseCtxResult : baseCtxResult);
157
166
  if (match.params) ctx.params = match.params;
158
- const reqTrace = analyticsTraceMap.get(request);
159
- if (reqTrace) ctx.__analyticsTrace = reqTrace;
167
+ const prepareResult = awaitHook("request:prepare", {
168
+ request,
169
+ ctx
170
+ });
171
+ if (prepareResult) await prepareResult;
160
172
  if (!route.passthrough) rawInput = await parseInput(request, url, qMark);
161
173
  if (match.params && Object.keys(match.params).length > 0) rawInput = rawInput != null && typeof rawInput === "object" ? {
162
174
  ...match.params,
@@ -166,17 +178,21 @@ function createFetchHandler(routerDef, contextFactory, hooks, prefix) {
166
178
  path: pathname,
167
179
  input: rawInput
168
180
  });
169
- const pipelineResult = runWithCtx(ctx, () => route.handler(ctx, rawInput, request.signal));
181
+ const pipelineResult = bridge ? bridge.run(ctx, () => route.handler(ctx, rawInput, request.signal)) : route.handler(ctx, rawInput, request.signal);
170
182
  const output = pipelineResult instanceof Promise ? await pipelineResult : pipelineResult;
171
183
  callHook("response", {
172
184
  path: pathname,
173
185
  output,
174
186
  durationMs: 0
175
187
  });
188
+ callHook("response:finalize", {
189
+ request,
190
+ ctx,
191
+ output
192
+ });
176
193
  const response = makeResponse(output, route, format, ctx);
177
194
  return response instanceof Promise ? await response : response;
178
195
  } catch (error) {
179
- releaseContext(ctx);
180
196
  callHook("error", {
181
197
  path: pathname,
182
198
  error
@@ -0,0 +1,131 @@
1
+ import { AnySchema } from "./schema.mjs";
2
+
3
+ //#region src/core/schema-converter.d.ts
4
+ /**
5
+ * JSON Schema subset used for OpenAPI / analytics output.
6
+ *
7
+ * @category Schema
8
+ */
9
+ interface JSONSchema {
10
+ type?: string | string[];
11
+ format?: string;
12
+ properties?: Record<string, JSONSchema>;
13
+ required?: string[];
14
+ items?: JSONSchema;
15
+ anyOf?: JSONSchema[];
16
+ oneOf?: JSONSchema[];
17
+ allOf?: JSONSchema[];
18
+ enum?: unknown[];
19
+ const?: unknown;
20
+ description?: string;
21
+ title?: string;
22
+ default?: unknown;
23
+ [key: string]: unknown;
24
+ }
25
+ /**
26
+ * Options passed to a converter's `toJsonSchema` method.
27
+ *
28
+ * @category Schema
29
+ */
30
+ interface ConvertOptions {
31
+ strategy: 'input' | 'output';
32
+ }
33
+ /**
34
+ * A converter that translates a specific Standard Schema vendor's schemas
35
+ * into JSON Schema. Pass instances via `silgi({ schemaConverters: [...] })`.
36
+ *
37
+ * @remarks
38
+ * Implement this interface to add OpenAPI / analytics support for a custom
39
+ * schema library. The `vendor` string must match the `~standard.vendor`
40
+ * property reported by the schema library's Standard Schema implementation.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import type { SchemaConverter } from 'silgi'
45
+ *
46
+ * const myConverter: SchemaConverter = {
47
+ * vendor: 'my-lib',
48
+ * toJsonSchema(schema, opts) {
49
+ * return { type: 'string' }
50
+ * },
51
+ * }
52
+ * ```
53
+ *
54
+ * @category Schema
55
+ */
56
+ interface SchemaConverter {
57
+ /** The Standard Schema `~standard.vendor` string this converter handles (e.g. `"zod"`). */
58
+ vendor: string;
59
+ /**
60
+ * Convert a schema to a JSON Schema object.
61
+ *
62
+ * @param schema - The schema to convert.
63
+ * @param opts - Conversion options including `strategy` (`'input'` | `'output'`).
64
+ * @returns A JSON Schema object. Return `{}` for unsupported/unknown schemas.
65
+ */
66
+ toJsonSchema(schema: AnySchema, opts: ConvertOptions): JSONSchema;
67
+ }
68
+ /**
69
+ * A per-instance registry mapping vendor strings to their converters.
70
+ *
71
+ * Built by {@link createSchemaRegistry} and threaded through the handler
72
+ * pipeline to scalar and analytics wrappers.
73
+ *
74
+ * @category Schema
75
+ */
76
+ type SchemaRegistry = Map<string, SchemaConverter>;
77
+ /**
78
+ * Build a {@link SchemaRegistry} from an array of converters.
79
+ *
80
+ * @param converters - Array of {@link SchemaConverter} objects, each
81
+ * declaring their own `vendor`.
82
+ * @returns A `Map<string, SchemaConverter>` keyed by `converter.vendor`.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * import { zodConverter } from 'silgi/zod'
87
+ * import { createSchemaRegistry } from 'silgi'
88
+ *
89
+ * const registry = createSchemaRegistry([zodConverter])
90
+ * ```
91
+ *
92
+ * @category Schema
93
+ */
94
+ declare function createSchemaRegistry(converters?: SchemaConverter[]): SchemaRegistry;
95
+ /**
96
+ * Convert any Standard Schema to JSON Schema.
97
+ *
98
+ * @remarks
99
+ * Resolution order:
100
+ * 1. **Native fast path** — `schema['~standard'].jsonSchema.input()`
101
+ * (Valibot, ArkType, Zod v4, …). No registry needed.
102
+ * 2. **Registry lookup** — finds a converter by
103
+ * `schema['~standard'].vendor`. Registry must be passed explicitly;
104
+ * there is no global mutable state.
105
+ * 3. **Empty schema `{}`** — emits a one-time `console.warn` per vendor
106
+ * when a registry was provided but contained no matching converter.
107
+ * No warn when no registry was passed (caller opted out).
108
+ *
109
+ * @param schema - Any Standard Schema compatible schema object.
110
+ * @param strategy - `'input'` (default) for pre-transform types; `'output'`
111
+ * for post-transform.
112
+ * @param registry - Optional {@link SchemaRegistry} built from
113
+ * {@link createSchemaRegistry}. When omitted the function still handles
114
+ * schemas that expose the native `jsonSchema.input()` fast path.
115
+ * @returns A JSON Schema object. Returns `{}` when conversion is not possible.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * import { zodConverter } from 'silgi/zod'
120
+ * import { createSchemaRegistry, schemaToJsonSchema } from 'silgi'
121
+ * import { z } from 'zod'
122
+ *
123
+ * const registry = createSchemaRegistry([zodConverter])
124
+ * const json = schemaToJsonSchema(z.object({ name: z.string() }), 'input', registry)
125
+ * ```
126
+ *
127
+ * @category Schema
128
+ */
129
+ declare function schemaToJsonSchema(schema: AnySchema, strategy?: 'input' | 'output', registry?: SchemaRegistry): JSONSchema;
130
+ //#endregion
131
+ export { ConvertOptions, JSONSchema, SchemaConverter, SchemaRegistry, createSchemaRegistry, schemaToJsonSchema };
@@ -0,0 +1,82 @@
1
+ //#region src/core/schema-converter.ts
2
+ /**
3
+ * Build a {@link SchemaRegistry} from an array of converters.
4
+ *
5
+ * @param converters - Array of {@link SchemaConverter} objects, each
6
+ * declaring their own `vendor`.
7
+ * @returns A `Map<string, SchemaConverter>` keyed by `converter.vendor`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { zodConverter } from 'silgi/zod'
12
+ * import { createSchemaRegistry } from 'silgi'
13
+ *
14
+ * const registry = createSchemaRegistry([zodConverter])
15
+ * ```
16
+ *
17
+ * @category Schema
18
+ */
19
+ function createSchemaRegistry(converters = []) {
20
+ const map = /* @__PURE__ */ new Map();
21
+ for (const converter of converters) map.set(converter.vendor, converter);
22
+ return map;
23
+ }
24
+ const _warnedVendors = /* @__PURE__ */ new Set();
25
+ /**
26
+ * Convert any Standard Schema to JSON Schema.
27
+ *
28
+ * @remarks
29
+ * Resolution order:
30
+ * 1. **Native fast path** — `schema['~standard'].jsonSchema.input()`
31
+ * (Valibot, ArkType, Zod v4, …). No registry needed.
32
+ * 2. **Registry lookup** — finds a converter by
33
+ * `schema['~standard'].vendor`. Registry must be passed explicitly;
34
+ * there is no global mutable state.
35
+ * 3. **Empty schema `{}`** — emits a one-time `console.warn` per vendor
36
+ * when a registry was provided but contained no matching converter.
37
+ * No warn when no registry was passed (caller opted out).
38
+ *
39
+ * @param schema - Any Standard Schema compatible schema object.
40
+ * @param strategy - `'input'` (default) for pre-transform types; `'output'`
41
+ * for post-transform.
42
+ * @param registry - Optional {@link SchemaRegistry} built from
43
+ * {@link createSchemaRegistry}. When omitted the function still handles
44
+ * schemas that expose the native `jsonSchema.input()` fast path.
45
+ * @returns A JSON Schema object. Returns `{}` when conversion is not possible.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { zodConverter } from 'silgi/zod'
50
+ * import { createSchemaRegistry, schemaToJsonSchema } from 'silgi'
51
+ * import { z } from 'zod'
52
+ *
53
+ * const registry = createSchemaRegistry([zodConverter])
54
+ * const json = schemaToJsonSchema(z.object({ name: z.string() }), 'input', registry)
55
+ * ```
56
+ *
57
+ * @category Schema
58
+ */
59
+ function schemaToJsonSchema(schema, strategy = "input", registry) {
60
+ const std = schema?.["~standard"];
61
+ if (std?.jsonSchema?.input) try {
62
+ const result = std.jsonSchema.input({ target: "draft-2020-12" });
63
+ if (result && typeof result === "object") {
64
+ const { $schema: _, ...rest } = result;
65
+ return rest;
66
+ }
67
+ } catch {}
68
+ const vendor = typeof std?.vendor === "string" ? std.vendor : void 0;
69
+ if (vendor && registry) {
70
+ const converter = registry.get(vendor);
71
+ if (converter) try {
72
+ return converter.toJsonSchema(schema, { strategy });
73
+ } catch {}
74
+ else if (!_warnedVendors.has(vendor)) {
75
+ _warnedVendors.add(vendor);
76
+ console.warn(`[silgi] No schema converter registered for vendor "${vendor}". Pass schemaConverters: [${vendor}Converter] to silgi() to enable OpenAPI / analytics schema generation.`);
77
+ }
78
+ }
79
+ return {};
80
+ }
81
+ //#endregion
82
+ export { createSchemaRegistry, schemaToJsonSchema };
@@ -26,8 +26,8 @@ interface ServeOptions {
26
26
  hostname?: string;
27
27
  /** Enable Scalar API Reference UI at /api/reference and /api/openapi.json */
28
28
  scalar?: boolean | ScalarOptions;
29
- /** Enable analytics dashboard at /api/analytics */
30
- analytics?: boolean | AnalyticsOptions;
29
+ /** Enable analytics dashboard at /api/analytics — requires `auth` to be set */
30
+ analytics?: AnalyticsOptions;
31
31
  /**
32
32
  * WebSocket RPC configuration.
33
33
  *
@@ -14,11 +14,18 @@ function routerHasSubscription(def) {
14
14
  for (const v of Object.values(def)) if (routerHasSubscription(v)) return true;
15
15
  return false;
16
16
  }
17
- async function createServeHandler(routerDef, contextFactory, hooks, options) {
17
+ async function createServeHandler(routerDef, contextFactory, hooks, options, schemaRegistry, bridge) {
18
18
  const port = options?.port ?? 3e3;
19
19
  const hostname = options?.hostname ?? "127.0.0.1";
20
20
  const prefix = options?.basePath ? normalizePrefix(options.basePath) : void 0;
21
- const httpHandler = wrapHandler(createFetchHandler(routerDef, contextFactory, hooks, prefix), routerDef, options, prefix);
21
+ const httpHandler = wrapHandler(createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge), routerDef, options ? {
22
+ ...options,
23
+ schemaRegistry,
24
+ hooks
25
+ } : {
26
+ schemaRegistry,
27
+ hooks
28
+ }, prefix);
22
29
  const shutdownOpt = options?.gracefulShutdown ?? true;
23
30
  let gracefulShutdown;
24
31
  if (typeof shutdownOpt === "object") gracefulShutdown = {
@@ -24,13 +24,13 @@ function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFac
24
24
  const dispatch = async (rawInput, parentCtx) => {
25
25
  const input = inputSchema ? await validateSchema(inputSchema, rawInput) : rawInput;
26
26
  const ctx = contextFactory ? await contextFactory() : {};
27
- const parentTrace = parentCtx?.__analyticsTrace;
27
+ const parentTrace = parentCtx?.trace;
28
28
  const spanStart = parentTrace ? performance.now() : 0;
29
29
  let reqTrace = null;
30
30
  try {
31
31
  const { RequestTrace } = await import("../plugins/analytics.mjs");
32
32
  reqTrace = new RequestTrace();
33
- ctx.__analyticsTrace = reqTrace;
33
+ ctx.trace = reqTrace;
34
34
  } catch {}
35
35
  const t0 = performance.now();
36
36
  try {
package/dist/index.d.mts CHANGED
@@ -1,12 +1,15 @@
1
1
  import { AnySchema, InferSchemaInput, InferSchemaOutput, Schema, ValidationError, type, validateSchema } from "./core/schema.mjs";
2
2
  import { ScheduledTaskInfo, TaskDef, TaskEvent, collectCronTasks, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
3
3
  import { ErrorDef, ErrorDefItem, FailFn, GuardDef, GuardFn, InferClient, InferContextFromUse, InferGuardOutput, Meta, MiddlewareDef, ProcedureDef, ProcedureType, ResolveContext, RouterDef, WrapDef, WrapFn } from "./types.mjs";
4
+ import { ConvertOptions, JSONSchema, SchemaConverter, SchemaRegistry, createSchemaRegistry, schemaToJsonSchema } from "./core/schema-converter.mjs";
4
5
  import { ScalarOptions, generateOpenAPI, scalarHTML } from "./scalar.mjs";
5
6
  import { ProcedureBuilder, ProcedureBuilderWithOutput } from "./builder.mjs";
7
+ import { ContextBridge, createContextBridge } from "./core/context-bridge.mjs";
6
8
  import { ServeOptions, SilgiServer } from "./core/serve.mjs";
7
9
  import { Driver, Storage, StorageConfig, StorageValue, initStorage, resetStorage, useStorage } from "./core/storage.mjs";
8
10
  import { SilgiConfig, SilgiInstance, silgi } from "./silgi.mjs";
9
- import { SilgiError, SilgiErrorCode, SilgiErrorJSON, SilgiErrorOptions, isDefinedError, toSilgiError } from "./core/error.mjs";
11
+ import { SilgiError, SilgiErrorCode, SilgiErrorJSON, SilgiErrorOptions, isDefinedError, isSilgiError, toSilgiError } from "./core/error.mjs";
12
+ import { BaseContext } from "./core/context.mjs";
10
13
  import { AsyncIteratorClass, mapAsyncIterator } from "./core/iterator.mjs";
11
14
  import { EventMeta, getEventMeta, withEventMeta } from "./core/sse.mjs";
12
15
  import { CallableOptions, callable } from "./callable.mjs";
@@ -14,4 +17,4 @@ import { LifecycleHooks, lifecycleWrap } from "./lifecycle.mjs";
14
17
  import { mapInput } from "./map-input.mjs";
15
18
  import { compileProcedure, compileRouter, createContext } from "./compile.mjs";
16
19
  import { LazyRouter, isLazy, lazy, resolveLazy } from "./lazy.mjs";
17
- export { type AnySchema, AsyncIteratorClass, type CallableOptions, type Driver, type ErrorDef, type ErrorDefItem, type EventMeta, type FailFn, type GuardDef, type GuardFn, type InferClient, type InferContextFromUse, type InferGuardOutput, type InferSchemaInput, type InferSchemaOutput, type LazyRouter, type LifecycleHooks, type Meta, type MiddlewareDef, type ProcedureBuilder, type ProcedureBuilderWithOutput, type ProcedureDef, type ProcedureType, type ResolveContext, type RouterDef, type ScalarOptions, type ScheduledTaskInfo, type Schema, type ServeOptions, type SilgiConfig, SilgiError, type SilgiErrorCode, type SilgiErrorJSON, type SilgiErrorOptions, type SilgiInstance, type SilgiServer, type Storage, type StorageConfig, type StorageValue, type TaskDef, type TaskEvent, ValidationError, type WrapDef, type WrapFn, callable, collectCronTasks, compileProcedure, compileRouter, createContext, generateOpenAPI, getEventMeta, getScheduledTasks, initStorage, isDefinedError, isLazy, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
20
+ export { type AnySchema, AsyncIteratorClass, type BaseContext, type CallableOptions, type ContextBridge, type ConvertOptions, type Driver, type ErrorDef, type ErrorDefItem, type EventMeta, type FailFn, type GuardDef, type GuardFn, type InferClient, type InferContextFromUse, type InferGuardOutput, type InferSchemaInput, type InferSchemaOutput, type JSONSchema, type LazyRouter, type LifecycleHooks, type Meta, type MiddlewareDef, type ProcedureBuilder, type ProcedureBuilderWithOutput, type ProcedureDef, type ProcedureType, type ResolveContext, type RouterDef, type ScalarOptions, type ScheduledTaskInfo, type Schema, type SchemaConverter, type SchemaRegistry, type ServeOptions, type SilgiConfig, SilgiError, type SilgiErrorCode, type SilgiErrorJSON, type SilgiErrorOptions, type SilgiInstance, type SilgiServer, type Storage, type StorageConfig, type StorageValue, type TaskDef, type TaskEvent, ValidationError, type WrapDef, type WrapFn, callable, collectCronTasks, compileProcedure, compileRouter, createContext, createContextBridge, createSchemaRegistry, generateOpenAPI, getEventMeta, getScheduledTasks, initStorage, isDefinedError, isLazy, isSilgiError, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, schemaToJsonSchema, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
package/dist/index.mjs CHANGED
@@ -1,9 +1,11 @@
1
1
  import { ValidationError, type, validateSchema } from "./core/schema.mjs";
2
2
  import { collectCronTasks, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
3
- import { SilgiError, isDefinedError, toSilgiError } from "./core/error.mjs";
3
+ import { SilgiError, isDefinedError, isSilgiError, toSilgiError } from "./core/error.mjs";
4
4
  import { compileProcedure, compileRouter, createContext } from "./compile.mjs";
5
+ import { createContextBridge } from "./core/context-bridge.mjs";
5
6
  import { AsyncIteratorClass, mapAsyncIterator } from "./core/iterator.mjs";
6
7
  import { getEventMeta, withEventMeta } from "./core/sse.mjs";
8
+ import { createSchemaRegistry, schemaToJsonSchema } from "./core/schema-converter.mjs";
7
9
  import { silgi } from "./silgi.mjs";
8
10
  import { callable } from "./callable.mjs";
9
11
  import { lifecycleWrap } from "./lifecycle.mjs";
@@ -11,4 +13,4 @@ import { mapInput } from "./map-input.mjs";
11
13
  import { isLazy, lazy, resolveLazy } from "./lazy.mjs";
12
14
  import { initStorage, resetStorage, useStorage } from "./core/storage.mjs";
13
15
  import { generateOpenAPI, scalarHTML } from "./scalar.mjs";
14
- export { AsyncIteratorClass, SilgiError, ValidationError, callable, collectCronTasks, compileProcedure, compileRouter, createContext, generateOpenAPI, getEventMeta, getScheduledTasks, initStorage, isDefinedError, isLazy, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
16
+ export { AsyncIteratorClass, SilgiError, ValidationError, callable, collectCronTasks, compileProcedure, compileRouter, createContext, createContextBridge, createSchemaRegistry, generateOpenAPI, getEventMeta, getScheduledTasks, initStorage, isDefinedError, isLazy, isSilgiError, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, schemaToJsonSchema, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
@@ -1,4 +1,25 @@
1
1
  //#region src/integrations/better-auth/index.d.ts
2
+ /**
3
+ * Associate a silgi context with a `Request` so the Better Auth tracing
4
+ * plugin can read it without mutating the `Request` object.
5
+ *
6
+ * @remarks
7
+ * Prefer this helper over the legacy `(request as any).__silgiCtx = ctx`
8
+ * assignment. The underlying storage is a module-level `WeakMap`, so
9
+ * entries are released automatically when the `Request` is GC'd.
10
+ *
11
+ * @param request - The `Request` currently being handled.
12
+ * @param ctx - The silgi context to associate, typically including `trace`.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { setRequestContext } from 'silgi/better-auth'
17
+ *
18
+ * setRequestContext(request, ctx)
19
+ * await auth.handler(request)
20
+ * ```
21
+ */
22
+ declare function setRequestContext(request: Request, ctx: Record<string, unknown>): void;
2
23
  interface TracingConfig {
3
24
  /** Capture request body as span input (default: true) */
4
25
  captureInput?: boolean;
@@ -38,4 +59,4 @@ declare function instrumentBetterAuth<T extends Record<string, any>>(auth: T): T
38
59
  */
39
60
  declare function withCtx<T>(ctx: Record<string, unknown>, fn: () => T): T;
40
61
  //#endregion
41
- export { TracingConfig, instrumentBetterAuth, tracing, withCtx };
62
+ export { TracingConfig, instrumentBetterAuth, setRequestContext, tracing, withCtx };
@@ -1,4 +1,4 @@
1
- import { getCtx, runWithCtx } from "../../core/context-bridge.mjs";
1
+ import { createContextBridge } from "../../core/context-bridge.mjs";
2
2
  //#region src/integrations/better-auth/index.ts
3
3
  /**
4
4
  * Silgi + Better Auth tracing integration.
@@ -6,20 +6,88 @@ import { getCtx, runWithCtx } from "../../core/context-bridge.mjs";
6
6
  * Provides a Better Auth plugin factory that auto-traces all auth operations
7
7
  * (sign-in, sign-up, OAuth, session management, etc.) into silgi analytics.
8
8
  *
9
- * The silgi request context is passed via `request.__silgiCtx`, set by
10
- * the silgi auth handler before calling `auth.handler(request)`.
9
+ * The silgi request context is associated with a `Request` in three ways,
10
+ * tried in priority order:
11
+ * 1. {@link setRequestContext}(request, ctx) — the blessed API (GC-friendly
12
+ * WeakMap, no mutation of the `Request` object).
13
+ * 2. `(request as any).__silgiCtx = ctx` — legacy property assignment; kept
14
+ * working for existing users but `setRequestContext` is preferred.
15
+ * 3. `silgi.runInContext(ctx, fn)` / `withCtx(ctx, fn)` — AsyncLocalStorage
16
+ * fallback when no Request is in scope (e.g., background jobs).
11
17
  *
12
18
  * @example
13
19
  * ```ts
14
- * import { tracing } from 'silgi/better-auth'
20
+ * import { tracing, setRequestContext } from 'silgi/better-auth'
15
21
  *
16
22
  * const auth = betterAuth({
17
- * plugins: [
18
- * tracing(), // auto-traces all auth operations
19
- * ],
23
+ * plugins: [tracing()],
20
24
  * })
25
+ *
26
+ * // In your silgi handler:
27
+ * setRequestContext(request, ctx)
28
+ * await auth.handler(request)
21
29
  * ```
22
30
  */
31
+ /**
32
+ * Module-level compat bridge — backs the exported `withCtx()` helper so
33
+ * tests and programmatic callers can still install an ambient context
34
+ * without needing a silgi instance. The request path in `handler.ts`
35
+ * uses `silgi.runInContext` on the per-instance bridge and never flows
36
+ * through this one, so there is no inter-instance context collision.
37
+ *
38
+ * @internal
39
+ */
40
+ const _compatBridge = createContextBridge();
41
+ function getCtx() {
42
+ return _compatBridge.current();
43
+ }
44
+ function runWithCtx(ctx, fn) {
45
+ return _compatBridge.run(ctx, fn);
46
+ }
47
+ /**
48
+ * Keyed on `Request` identity. GC-friendly: entry auto-released when the
49
+ * `Request` is collected.
50
+ *
51
+ * @internal
52
+ */
53
+ const _requestContextMap = /* @__PURE__ */ new WeakMap();
54
+ /**
55
+ * Associate a silgi context with a `Request` so the Better Auth tracing
56
+ * plugin can read it without mutating the `Request` object.
57
+ *
58
+ * @remarks
59
+ * Prefer this helper over the legacy `(request as any).__silgiCtx = ctx`
60
+ * assignment. The underlying storage is a module-level `WeakMap`, so
61
+ * entries are released automatically when the `Request` is GC'd.
62
+ *
63
+ * @param request - The `Request` currently being handled.
64
+ * @param ctx - The silgi context to associate, typically including `trace`.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * import { setRequestContext } from 'silgi/better-auth'
69
+ *
70
+ * setRequestContext(request, ctx)
71
+ * await auth.handler(request)
72
+ * ```
73
+ */
74
+ function setRequestContext(request, ctx) {
75
+ _requestContextMap.set(request, ctx);
76
+ }
77
+ /**
78
+ * Internal resolver that tries, in order: WeakMap (new API), legacy
79
+ * `__silgiCtx` property (deprecated but supported), and finally the
80
+ * integration's compat bridge ALS.
81
+ *
82
+ * @internal
83
+ */
84
+ function resolveRequestContext(request) {
85
+ const fromMap = _requestContextMap.get(request);
86
+ if (fromMap) return fromMap;
87
+ const legacy = request.__silgiCtx;
88
+ if (legacy && typeof legacy === "object") return legacy;
89
+ return getCtx();
90
+ }
23
91
  function matchOperation(path) {
24
92
  const normalized = path.replace(/^\/+/, "");
25
93
  if (normalized.endsWith("/sign-up/email") || normalized === "sign-up/email") return {
@@ -151,9 +219,9 @@ function tracing(config) {
151
219
  try {
152
220
  const request = ctx.request;
153
221
  if (!request) return;
154
- const silgiCtx = request.__silgiCtx ?? getCtx();
222
+ const silgiCtx = resolveRequestContext(request);
155
223
  if (!silgiCtx) return;
156
- const reqTrace = silgiCtx.__analyticsTrace;
224
+ const reqTrace = silgiCtx.trace;
157
225
  if (!reqTrace) return;
158
226
  const meta = requestMetas.get(request);
159
227
  requestMetas.delete(request);
@@ -284,7 +352,7 @@ function withCtx(ctx, fn) {
284
352
  }
285
353
  function wrapApiMethod(originalFn, operation, method) {
286
354
  return async function instrumented(...args) {
287
- const reqTrace = getCtx()?.__analyticsTrace;
355
+ const reqTrace = getCtx()?.trace;
288
356
  if (!reqTrace) return originalFn.apply(this, args);
289
357
  const spanName = `auth.api.${operation}`;
290
358
  const start = performance.now();
@@ -328,4 +396,4 @@ function wrapApiMethod(originalFn, operation, method) {
328
396
  };
329
397
  }
330
398
  //#endregion
331
- export { instrumentBetterAuth, tracing, withCtx };
399
+ export { instrumentBetterAuth, setRequestContext, tracing, withCtx };
@@ -1,4 +1,4 @@
1
- import { getCtx, runWithCtx } from "../../core/context-bridge.mjs";
1
+ import { createContextBridge } from "../../core/context-bridge.mjs";
2
2
  //#region src/integrations/drizzle/index.ts
3
3
  /**
4
4
  * Silgi + Drizzle ORM tracing integration.
@@ -31,6 +31,23 @@ import { getCtx, runWithCtx } from "../../core/context-bridge.mjs";
31
31
  * })
32
32
  * ```
33
33
  */
34
+ /**
35
+ * Module-level compat bridge — backs the exported `withCtx()` helper for
36
+ * programmatic and test usage. Production request paths flow through
37
+ * `silgi.runInContext()` via `src/core/handler.ts` and never touch this
38
+ * bridge, so there is no cross-instance context bleed during normal
39
+ * operation. The bridge exists only so `withCtx(ctx, fn)` works without
40
+ * a silgi instance in scope.
41
+ *
42
+ * @internal
43
+ */
44
+ const _compatBridge = createContextBridge();
45
+ function getCtx() {
46
+ return _compatBridge.current();
47
+ }
48
+ function runWithCtx(ctx, fn) {
49
+ return _compatBridge.run(ctx, fn);
50
+ }
34
51
  const INSTRUMENTED = "__silgiDrizzleInstrumented";
35
52
  const DEFAULT_DB_SYSTEM = "postgresql";
36
53
  const DEFAULT_MAX_QUERY_LENGTH = 1e3;
@@ -83,7 +100,7 @@ function patchSession(session, cfg, isTx) {
83
100
  session.prepareQuery = function patchedPrepareQuery(...args) {
84
101
  const prepared = originalPrepareQuery.apply(this, args);
85
102
  if (!prepared || typeof prepared.execute !== "function") return prepared;
86
- const reqTrace = getCtx()?.__analyticsTrace;
103
+ const reqTrace = getCtx()?.trace;
87
104
  if (!reqTrace) return prepared;
88
105
  const queryText = extractQueryText(args[0]) ?? prepared.rawQueryConfig?.text ?? prepared.queryConfig?.text ?? null;
89
106
  const originalExecute = prepared.execute.bind(prepared);
@@ -97,7 +114,7 @@ function patchSession(session, cfg, isTx) {
97
114
  if (typeof session.query === "function") {
98
115
  const originalQuery = session.query.bind(session);
99
116
  session.query = function patchedQuery(queryString, params) {
100
- const reqTrace = getCtx()?.__analyticsTrace;
117
+ const reqTrace = getCtx()?.trace;
101
118
  if (!reqTrace) return originalQuery.call(this, queryString, params);
102
119
  return traceExecution(reqTrace, cfg, queryString ?? null, isTx, originalQuery, this, [queryString, params]);
103
120
  };
@@ -126,7 +143,7 @@ function patchRawClient(client, cfg) {
126
143
  if (!methodName) return false;
127
144
  const originalMethod = client[methodName].bind(client);
128
145
  client[methodName] = function patchedClientMethod(...args) {
129
- const reqTrace = getCtx()?.__analyticsTrace;
146
+ const reqTrace = getCtx()?.trace;
130
147
  if (!reqTrace) return originalMethod.apply(this, args);
131
148
  return traceExecution(reqTrace, cfg, extractQueryText(args[0]) ?? null, false, originalMethod, this, args);
132
149
  };
@@ -140,7 +157,7 @@ function patchSessionExecute(session, cfg) {
140
157
  if (session[INSTRUMENTED]) return false;
141
158
  const originalExecute = session.execute.bind(session);
142
159
  session.execute = function patchedDeepExecute(...args) {
143
- const reqTrace = getCtx()?.__analyticsTrace;
160
+ const reqTrace = getCtx()?.trace;
144
161
  if (!reqTrace) return originalExecute.apply(this, args);
145
162
  return traceExecution(reqTrace, cfg, extractQueryText(args[0]) ?? null, false, originalExecute, this, args);
146
163
  };
@@ -72,4 +72,4 @@ declare class ZodSchemaConverter implements SchemaConverter {
72
72
  convert(schema: AnySchema, options: ConvertOptions): [boolean, JSONSchema];
73
73
  }
74
74
  //#endregion
75
- export { CompositeSchemaConverter, ConvertOptions, JSONSchema, SchemaConverter, ZodSchemaConverter };
75
+ export { CompositeSchemaConverter, ConvertOptions, JSONSchema, ZodSchemaConverter };