silgi 0.52.2 → 0.53.1

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/scalar.mjs CHANGED
@@ -2,27 +2,43 @@ import { collectProcedures } from "./core/router-utils.mjs";
2
2
  import { schemaToJsonSchema } from "./core/schema-converter.mjs";
3
3
  //#region src/scalar.ts
4
4
  /**
5
- * Scalar API Reference v2 OpenAPI integration.
5
+ * Scalar API Reference + OpenAPI 3.1.0 generation
6
+ * --------------------------------------------------
6
7
  *
7
- * Generates OpenAPI 3.1.0 spec from v2 RouterDef and serves
8
- * Scalar UI at /api/reference + spec at /api/openapi.json.
9
- */
10
- /**
11
- * Generate OpenAPI 3.1.0 document from a v2 RouterDef.
8
+ * Walks a silgi `RouterDef` and emits an OpenAPI 3.1.0 document, then
9
+ * wraps a Fetch handler so that two additional routes are served:
10
+ *
11
+ * `{basePath}/openapi.json` — the raw spec.
12
+ * `{basePath}/reference` — the Scalar API Reference UI (HTML).
13
+ *
14
+ * Every procedure becomes one OpenAPI operation per HTTP method. Input
15
+ * schemas become `parameters[in:query]` for GET and `requestBody` for
16
+ * the rest. Typed errors declared via `.$errors()` become typed response
17
+ * shapes grouped by status code. Subscriptions are documented as SSE
18
+ * endpoints (clients are still expected to use the native WebSocket
19
+ * channel; the REST representation is for discoverability).
20
+ *
21
+ * The document respects per-procedure escape hatches in that order:
22
+ *
23
+ * `route.spec` — either a function `(op) => op` or an object merge.
24
+ * `route.security` — per-op security override (or `false` for public).
25
+ * `route.successStatus` / `route.successDescription` — success shape.
12
26
  */
13
27
  /**
14
- * Convert `:param` and `:param(regex)` to OpenAPI `{param}` syntax.
15
- * Returns the converted path and an array of extracted param names.
28
+ * Convert silgi's `:param`, `:param?`, `:param(regex)`, and `**`
29
+ * route syntax to OpenAPI's `{param}` form, and collect the extracted
30
+ * parameter names so callers can list them in the operation's
31
+ * `parameters`.
16
32
  */
17
33
  function toOpenAPIPath(raw) {
18
34
  const pathParams = [];
19
- const httpPath = raw.replace(/:(\w+)\([^)]*\)/g, (_m, name) => {
35
+ const httpPath = raw.replace(/:(\w+)\([^)]*\)/g, (_match, name) => {
20
36
  pathParams.push(name);
21
37
  return `{${name}}`;
22
- }).replace(/:(\w+)\?/g, (_m, name) => {
38
+ }).replace(/:(\w+)\?/g, (_match, name) => {
23
39
  pathParams.push(name);
24
40
  return `{${name}}`;
25
- }).replace(/:(\w+)/g, (_m, name) => {
41
+ }).replace(/:(\w+)/g, (_match, name) => {
26
42
  pathParams.push(name);
27
43
  return `{${name}}`;
28
44
  }).replace(/\/\*\*$/g, "/{path}").replace(/\/\*\*/g, "/{path}");
@@ -32,208 +48,286 @@ function toOpenAPIPath(raw) {
32
48
  pathParams
33
49
  };
34
50
  }
51
+ const HTTP_METHODS = [
52
+ "get",
53
+ "put",
54
+ "post",
55
+ "delete",
56
+ "options",
57
+ "head",
58
+ "patch",
59
+ "trace"
60
+ ];
61
+ /** Escape a string for inclusion in HTML attribute or text content. */
62
+ function escapeHtml(s) {
63
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
64
+ }
65
+ /**
66
+ * Turn a flat object schema into OpenAPI query-parameter entries. Used
67
+ * for `GET` routes: the body is moved to the URL.
68
+ */
69
+ function objectSchemaToParams(schema) {
70
+ if (schema.type !== "object" || !schema.properties) return [];
71
+ const required = new Set(schema.required ?? []);
72
+ return Object.entries(schema.properties).map(([name, propSchema]) => ({
73
+ name,
74
+ in: "query",
75
+ required: required.has(name),
76
+ schema: propSchema,
77
+ ...propSchema.description ? { description: propSchema.description } : {}
78
+ }));
79
+ }
80
+ /**
81
+ * Collect every typed error declared by a procedure *and* by any guard
82
+ * it uses, keyed by HTTP status. Guard errors merge into the procedure
83
+ * errors on a collision (procedure wins).
84
+ */
85
+ function collectTypedErrors(proc) {
86
+ const guards = (proc.use ?? []).filter((m) => typeof m === "object" && m !== null && m.kind === "guard" && !!m.errors);
87
+ let merged = proc.errors ? { ...proc.errors } : null;
88
+ for (const guard of guards) merged = merged ? {
89
+ ...merged,
90
+ ...guard.errors
91
+ } : { ...guard.errors };
92
+ return merged;
93
+ }
94
+ /** Group typed errors by status code; each status may carry N codes. */
95
+ function groupErrorsByStatus(errors, schemaToJson) {
96
+ const byStatus = /* @__PURE__ */ new Map();
97
+ for (const [code, rawDef] of Object.entries(errors)) {
98
+ const status = typeof rawDef === "number" ? rawDef : rawDef.status;
99
+ if (!byStatus.has(status)) byStatus.set(status, []);
100
+ const entry = { code };
101
+ if (typeof rawDef === "object" && rawDef !== null) {
102
+ const def = rawDef;
103
+ if (def.message) entry.message = def.message;
104
+ if (def.data) entry.schema = schemaToJson(def.data);
105
+ }
106
+ byStatus.get(status).push(entry);
107
+ }
108
+ return byStatus;
109
+ }
110
+ /** Build one response-object schema for a typed error. */
111
+ function errorEntryToJsonSchema(entry, status) {
112
+ const schema = {
113
+ type: "object",
114
+ properties: {
115
+ code: {
116
+ const: entry.code,
117
+ type: "string"
118
+ },
119
+ status: {
120
+ const: status,
121
+ type: "integer"
122
+ },
123
+ message: {
124
+ type: "string",
125
+ ...entry.message ? { default: entry.message } : {}
126
+ }
127
+ },
128
+ required: [
129
+ "code",
130
+ "status",
131
+ "message"
132
+ ]
133
+ };
134
+ if (entry.schema) {
135
+ schema.properties.data = entry.schema;
136
+ schema.required.push("data");
137
+ }
138
+ return schema;
139
+ }
140
+ /**
141
+ * Build one OpenAPI operation object (the value for `paths[p][method]`).
142
+ *
143
+ * Parameters are path-params first, then query-params for GETs. Body
144
+ * goes on the request side for non-GETs. Responses are the declared
145
+ * success status plus an auto-documented 400 (when input validation
146
+ * exists) plus one slot per typed-error status.
147
+ */
148
+ function buildOperation(args) {
149
+ const { proc, path, method, inputSchema, schemaToJson } = args;
150
+ const op = {
151
+ operationId: args.operationId,
152
+ responses: {}
153
+ };
154
+ if (args.tags?.length) op.tags = args.tags;
155
+ if (args.summary) op.summary = args.summary;
156
+ if (args.description) op.description = args.description;
157
+ if (args.deprecated) op.deprecated = true;
158
+ const route = proc.route;
159
+ if (route?.security === false) op.security = [];
160
+ else if (route?.security) op.security = route.security.map((s) => ({ [s]: [] }));
161
+ else if (args.globalSecurity) op.security = [{ auth: [] }];
162
+ const parameters = args.pathParams.map((name) => ({
163
+ name,
164
+ in: "path",
165
+ required: true,
166
+ schema: { type: "string" }
167
+ }));
168
+ if (inputSchema) if (method === "get") parameters.push(...objectSchemaToParams(inputSchema));
169
+ else op.requestBody = {
170
+ required: true,
171
+ content: { "application/json": { schema: inputSchema } }
172
+ };
173
+ if (parameters.length > 0) op.parameters = parameters;
174
+ const responses = op.responses;
175
+ const successStatus = route?.successStatus ?? 200;
176
+ const successDesc = route?.successDescription ?? "Successful response";
177
+ if (proc.type === "subscription") {
178
+ const outputSchema = proc.output ? schemaToJson(proc.output, "output") : { type: "string" };
179
+ responses[String(successStatus)] = {
180
+ description: "SSE event stream",
181
+ content: { "text/event-stream": { schema: {
182
+ type: "string",
183
+ description: `Each line: data: ${JSON.stringify(outputSchema)}`
184
+ } } }
185
+ };
186
+ } else if (proc.output) responses[String(successStatus)] = {
187
+ description: successDesc,
188
+ content: { "application/json": { schema: schemaToJson(proc.output, "output") } }
189
+ };
190
+ else responses[String(successStatus)] = { description: successDesc };
191
+ if (proc.input) responses["400"] = {
192
+ description: "BAD_REQUEST — input validation failed",
193
+ content: { "application/json": { schema: {
194
+ type: "object",
195
+ properties: {
196
+ code: {
197
+ const: "BAD_REQUEST",
198
+ type: "string"
199
+ },
200
+ status: {
201
+ const: 400,
202
+ type: "integer"
203
+ },
204
+ message: { type: "string" },
205
+ data: {
206
+ type: "object",
207
+ properties: { issues: { type: "array" } }
208
+ }
209
+ },
210
+ required: [
211
+ "code",
212
+ "status",
213
+ "message"
214
+ ]
215
+ } } }
216
+ };
217
+ const typedErrors = collectTypedErrors(proc);
218
+ if (typedErrors) {
219
+ const byStatus = groupErrorsByStatus(typedErrors, schemaToJson);
220
+ for (const [status, entries] of byStatus) {
221
+ const schemas = entries.map((entry) => errorEntryToJsonSchema(entry, status));
222
+ responses[String(status)] = {
223
+ description: entries.map((e) => e.code).join(" | "),
224
+ content: { "application/json": { schema: schemas.length === 1 ? schemas[0] : { oneOf: schemas } } }
225
+ };
226
+ }
227
+ }
228
+ let finalOp = op;
229
+ if (route?.spec) finalOp = typeof route.spec === "function" ? route.spec(op) : {
230
+ ...op,
231
+ ...route.spec
232
+ };
233
+ return finalOp;
234
+ }
235
+ /** Build the `info` field of the doc, plus optional `servers` / `externalDocs`. */
236
+ function buildDocHeader(options) {
237
+ const info = {
238
+ title: options.title ?? "Silgi API",
239
+ version: options.version ?? "1.0.0"
240
+ };
241
+ if (options.description) info.description = options.description;
242
+ if (options.contact) info.contact = options.contact;
243
+ if (options.license) info.license = options.license;
244
+ const extras = {};
245
+ if (options.servers?.length) extras.servers = options.servers;
246
+ if (options.externalDocs) extras.externalDocs = options.externalDocs;
247
+ return {
248
+ info,
249
+ extras
250
+ };
251
+ }
252
+ /**
253
+ * Translate our compact `security` option into the OpenAPI
254
+ * `components.securitySchemes` shape. The key under which the scheme
255
+ * lives is hard-coded as `auth` — per-operation `security` values
256
+ * reference it by that name.
257
+ */
258
+ function buildSecurityScheme(security) {
259
+ const scheme = { type: security.type };
260
+ if (security.type === "http") {
261
+ scheme.scheme = security.scheme ?? "bearer";
262
+ if (security.bearerFormat) scheme.bearerFormat = security.bearerFormat;
263
+ } else if (security.type === "apiKey") {
264
+ scheme.in = security.in ?? "header";
265
+ scheme.name = security.name ?? "x-api-key";
266
+ }
267
+ if (security.description) scheme.description = security.description;
268
+ return { securitySchemes: { auth: scheme } };
269
+ }
270
+ /**
271
+ * Generate an OpenAPI 3.1.0 document for a `RouterDef`.
272
+ *
273
+ * The document is a plain object and can be re-serialized, cached,
274
+ * piped into any OpenAPI consumer (codegen, docs site, validators). We
275
+ * do not return a typed `OpenAPIV3_1.Document` because the typings in
276
+ * the ecosystem are noisy; downstream consumers usually only care
277
+ * about a handful of keys anyway.
278
+ */
35
279
  function generateOpenAPI(router, options = {}, basePath = "", registry) {
36
- const schemaToJsonSchema$1 = (schema, strategy = "input") => schemaToJsonSchema(schema, strategy, registry);
280
+ const schemaToJson = (schema, strategy = "input") => schemaToJsonSchema(schema, strategy, registry);
37
281
  const paths = {};
38
282
  const tags = /* @__PURE__ */ new Map();
39
283
  collectProcedures(router, (path, proc) => {
40
284
  const route = proc.route;
41
285
  const { httpPath: routePath, pathParams } = toOpenAPIPath(route?.path ?? "/" + path.join("/"));
42
286
  const httpPath = basePath ? basePath.replace(/\/$/, "") + routePath : routePath;
43
- const rawMethod = route?.method?.toLowerCase() ?? "post";
44
- const methods = rawMethod === "*" ? [
45
- "get",
46
- "put",
47
- "post",
48
- "delete",
49
- "options",
50
- "head",
51
- "patch",
52
- "trace"
53
- ] : [rawMethod];
287
+ const declaredMethod = route?.method?.toLowerCase() ?? "post";
288
+ const methods = declaredMethod === "*" ? [...HTTP_METHODS] : [declaredMethod];
54
289
  const baseOperationId = route?.operationId ?? path.join("_");
55
290
  const opTags = route?.tags ?? (path.length > 1 ? [path[0]] : void 0);
56
291
  if (opTags) {
57
- for (const t of opTags) if (!tags.has(t)) tags.set(t, {});
292
+ for (const tag of opTags) if (!tags.has(tag)) tags.set(tag, {});
58
293
  }
59
294
  let description = route?.description;
60
295
  if (proc.type === "subscription") {
61
296
  const wsNote = "Streams over WebSocket (`ws://…/_ws`). Send `{ id, path: \"" + path.join("/") + "\", input }` as JSON.";
62
297
  description = description ? `${description}\n\n${wsNote}` : wsNote;
63
298
  }
64
- const operation = {
65
- operationId: baseOperationId,
66
- tags: opTags,
67
- summary: route?.summary,
68
- description,
69
- deprecated: route?.deprecated || void 0,
70
- responses: {}
71
- };
72
- if (!operation.summary) delete operation.summary;
73
- if (!operation.description) delete operation.description;
74
- if (!operation.deprecated) delete operation.deprecated;
75
- if (!operation.tags) delete operation.tags;
76
- if (route?.security === false) operation.security = [];
77
- else if (route?.security) operation.security = route.security.map((s) => ({ [s]: [] }));
78
- else if (options.security) operation.security = [{ auth: [] }];
79
- const inputSchema = proc.input ? schemaToJsonSchema$1(proc.input, "input") : null;
80
- const successStatus = route?.successStatus ?? 200;
81
- const successDesc = route?.successDescription ?? "Successful response";
82
- const guards = (proc.use ?? []).filter((m) => m.kind === "guard" && m.errors);
83
- let allErrors = proc.errors ? { ...proc.errors } : null;
84
- for (const guard of guards) {
85
- const ge = guard.errors;
86
- if (ge) allErrors = allErrors ? {
87
- ...allErrors,
88
- ...ge
89
- } : { ...ge };
90
- }
299
+ const inputSchema = proc.input ? schemaToJson(proc.input, "input") : null;
91
300
  paths[httpPath] ??= {};
92
301
  for (const method of methods) {
93
- const op = {
94
- ...operation,
95
- responses: {}
96
- };
97
- if (methods.length > 1) op.operationId = `${baseOperationId}_${method}`;
98
- const params = [];
99
- for (const p of pathParams) params.push({
100
- name: p,
101
- in: "path",
102
- required: true,
103
- schema: { type: "string" }
302
+ const op = buildOperation({
303
+ proc,
304
+ path,
305
+ method,
306
+ operationId: methods.length > 1 ? `${baseOperationId}_${method}` : baseOperationId,
307
+ tags: opTags,
308
+ summary: route?.summary,
309
+ description,
310
+ deprecated: route?.deprecated || void 0,
311
+ pathParams,
312
+ inputSchema,
313
+ schemaToJson,
314
+ globalSecurity: options.security
104
315
  });
105
- if (inputSchema) if (method === "get") params.push(...objectSchemaToParams(inputSchema));
106
- else op.requestBody = {
107
- required: true,
108
- content: { "application/json": { schema: inputSchema } }
109
- };
110
- if (params.length > 0) op.parameters = params;
111
- if (proc.type === "subscription") {
112
- const outputSchema = proc.output ? schemaToJsonSchema$1(proc.output, "output") : { type: "string" };
113
- op.responses[String(successStatus)] = {
114
- description: "SSE event stream",
115
- content: { "text/event-stream": { schema: {
116
- type: "string",
117
- description: `Each line: data: ${JSON.stringify(outputSchema)}`
118
- } } }
119
- };
120
- } else if (proc.output) op.responses[String(successStatus)] = {
121
- description: successDesc,
122
- content: { "application/json": { schema: schemaToJsonSchema$1(proc.output, "output") } }
123
- };
124
- else op.responses[String(successStatus)] = { description: successDesc };
125
- if (proc.input) op.responses["400"] = {
126
- description: "BAD_REQUEST — input validation failed",
127
- content: { "application/json": { schema: {
128
- type: "object",
129
- properties: {
130
- code: {
131
- const: "BAD_REQUEST",
132
- type: "string"
133
- },
134
- status: {
135
- const: 400,
136
- type: "integer"
137
- },
138
- message: { type: "string" },
139
- data: {
140
- type: "object",
141
- properties: { issues: { type: "array" } }
142
- }
143
- },
144
- required: [
145
- "code",
146
- "status",
147
- "message"
148
- ]
149
- } } }
150
- };
151
- if (allErrors) {
152
- const byStatus = /* @__PURE__ */ new Map();
153
- for (const [code, def] of Object.entries(allErrors)) {
154
- const status = typeof def === "number" ? def : def.status;
155
- if (!byStatus.has(status)) byStatus.set(status, []);
156
- const entry = { code };
157
- if (typeof def === "object") {
158
- if (def.message) entry.message = def.message;
159
- if (def.data) entry.schema = schemaToJsonSchema$1(def.data);
160
- }
161
- byStatus.get(status).push(entry);
162
- }
163
- for (const [status, errors] of byStatus) {
164
- const errorSchemas = errors.map((e) => {
165
- const s = {
166
- type: "object",
167
- properties: {
168
- code: {
169
- const: e.code,
170
- type: "string"
171
- },
172
- status: {
173
- const: status,
174
- type: "integer"
175
- },
176
- message: {
177
- type: "string",
178
- ...e.message ? { default: e.message } : {}
179
- }
180
- },
181
- required: [
182
- "code",
183
- "status",
184
- "message"
185
- ]
186
- };
187
- if (e.schema) {
188
- s.properties.data = e.schema;
189
- s.required.push("data");
190
- }
191
- return s;
192
- });
193
- op.responses[String(status)] = {
194
- description: errors.map((e) => e.code).join(" | "),
195
- content: { "application/json": { schema: errorSchemas.length === 1 ? errorSchemas[0] : { oneOf: errorSchemas } } }
196
- };
197
- }
198
- }
199
- let finalOp = op;
200
- if (route?.spec) if (typeof route.spec === "function") finalOp = route.spec(finalOp);
201
- else finalOp = {
202
- ...finalOp,
203
- ...route.spec
204
- };
205
- paths[httpPath][method] = finalOp;
316
+ paths[httpPath][method] = op;
206
317
  }
207
318
  });
319
+ const { info, extras } = buildDocHeader(options);
208
320
  const doc = {
209
321
  openapi: "3.1.0",
210
- info: {
211
- title: options.title ?? "Silgi API",
212
- version: options.version ?? "1.0.0",
213
- ...options.description ? { description: options.description } : {},
214
- ...options.contact ? { contact: options.contact } : {},
215
- ...options.license ? { license: options.license } : {}
216
- },
217
- paths
322
+ info,
323
+ paths,
324
+ ...extras
218
325
  };
219
- if (options.servers?.length) doc.servers = options.servers;
220
- if (options.externalDocs) doc.externalDocs = options.externalDocs;
221
326
  if (tags.size > 0) doc.tags = [...tags.entries()].map(([name, meta]) => ({
222
327
  name,
223
328
  ...meta.description ? { description: meta.description } : {}
224
329
  }));
225
- if (options.security) {
226
- const scheme = { type: options.security.type };
227
- if (options.security.type === "http") {
228
- scheme.scheme = options.security.scheme ?? "bearer";
229
- if (options.security.bearerFormat) scheme.bearerFormat = options.security.bearerFormat;
230
- } else if (options.security.type === "apiKey") {
231
- scheme.in = options.security.in ?? "header";
232
- scheme.name = options.security.name ?? "x-api-key";
233
- }
234
- if (options.security.description) scheme.description = options.security.description;
235
- doc.components = { securitySchemes: { auth: scheme } };
236
- }
330
+ if (options.security) doc.components = buildSecurityScheme(options.security);
237
331
  return doc;
238
332
  }
239
333
  const SCALAR_CDN_SOURCES = {
@@ -241,6 +335,10 @@ const SCALAR_CDN_SOURCES = {
241
335
  unpkg: "https://unpkg.com/@scalar/api-reference",
242
336
  local: "/__silgi/scalar.js"
243
337
  };
338
+ /**
339
+ * Render the minimal HTML shell the Scalar UI needs. The UI itself is
340
+ * a single script that reads the `data-url` attribute to pull the spec.
341
+ */
244
342
  function scalarHTML(specUrl, options = {}) {
245
343
  const title = escapeHtml(options.title ?? "Silgi API");
246
344
  const safeUrl = escapeHtml(specUrl);
@@ -258,23 +356,16 @@ function scalarHTML(specUrl, options = {}) {
258
356
  </body>
259
357
  </html>`;
260
358
  }
261
- function escapeHtml(s) {
262
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
263
- }
264
- function objectSchemaToParams(schema) {
265
- if (schema.type !== "object" || !schema.properties) return [];
266
- const required = new Set(schema.required ?? []);
267
- return Object.entries(schema.properties).map(([name, propSchema]) => ({
268
- name,
269
- in: "query",
270
- required: required.has(name),
271
- schema: propSchema,
272
- ...propSchema.description ? { description: propSchema.description } : {}
273
- }));
274
- }
275
359
  /**
276
- * Wrap a fetch handler to serve Scalar API Reference at /api/reference and /api/openapi.json.
277
- * Scalar routes are intercepted before the handler — zero overhead for normal requests.
360
+ * Wrap a Fetch handler so that two extra paths are served:
361
+ *
362
+ * `{basePath}/reference` — the Scalar UI.
363
+ * `{basePath}/openapi.json` — the spec.
364
+ *
365
+ * The generated spec is JSON-stringified once at wrap time so every
366
+ * hit to `/openapi.json` is a constant-cost `new Response(cachedJson)`.
367
+ * Requests that do not match either path fall through to the inner
368
+ * handler with zero overhead.
278
369
  */
279
370
  function wrapWithScalar(handler, routerDef, options = {}, prefix = "/api", registry) {
280
371
  const normPrefix = (prefix.startsWith("/") ? prefix : "/" + prefix).replace(/\/+$/, "");
package/dist/silgi.d.mts CHANGED
@@ -85,6 +85,41 @@ interface SilgiConfig<TCtx extends Record<string, unknown>> {
85
85
  * ```
86
86
  */
87
87
  schemaConverters?: SchemaConverter[];
88
+ /**
89
+ * Root-level wrap middleware applied to every procedure in the router.
90
+ *
91
+ * @remarks
92
+ * Each entry must be created via `instance.wrap(fn)`. Root wraps run
93
+ * as the outermost layer of the onion: root wraps → route-level
94
+ * `.$use()` guards/wraps → resolver. Use this for concerns that must
95
+ * apply to every route (tenant scoping, `AsyncLocalStorage` setup,
96
+ * trace propagation), where missing one route would be a bug.
97
+ *
98
+ * Root wraps cannot mutate the context type — use a route-level
99
+ * `$use(guard)` for that. The ambient context passed to `next()` is
100
+ * `TBaseCtx`, identical to the one seen by route-level wraps.
101
+ *
102
+ * Applies to every procedure reachable through `handler()`,
103
+ * `createCaller()`, and HTTP/cron task invocation. Task `dispatch()`
104
+ * (programmatic, bypasses the pipeline) is not wrapped.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * const tenantScopeWrap: WrapDef = {
109
+ * kind: 'wrap',
110
+ * fn: (ctx, next) => tenantScope.run({ orgId: ctx.user.orgId }, next),
111
+ * }
112
+ *
113
+ * const s = silgi({
114
+ * context: (req) => ({ db, user: readUser(req) }),
115
+ * wraps: [tenantScopeWrap],
116
+ * })
117
+ * ```
118
+ *
119
+ * For convenience you can also create wraps with the standalone
120
+ * helper or from another silgi instance's `wrap()` method.
121
+ */
122
+ wraps?: WrapDef<TCtx>[];
88
123
  /**
89
124
  * Storage configuration — mount drivers by path prefix.
90
125
  *