silgi 0.51.7 → 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 +47 -11
  51. package/package.json +6 -2
  52. package/dist/core/trace-map.d.mts +0 -13
  53. package/dist/core/trace-map.mjs +0 -13
@@ -1,7 +1,37 @@
1
1
  //#region src/core/error.d.ts
2
2
  /**
3
- * SilgiError — unified RPC error with cross-realm instanceof.
3
+ * SilgiError — unified RPC error type for the Silgi framework.
4
+ *
5
+ * @remarks
6
+ * Every error thrown through the Silgi request pipeline is either a
7
+ * `SilgiError` or is wrapped into one by {@link toSilgiError} before it
8
+ * reaches the wire. The class carries an HTTP `status`, a machine-readable
9
+ * `code`, optional structured `data`, and a `defined` flag that signals
10
+ * whether the error was deliberately declared with `$errors()` (safe to
11
+ * expose verbatim to clients) or is an internal fault that should be
12
+ * redacted.
13
+ *
14
+ * ### Cross-realm `instanceof`
15
+ * `SilgiError` overrides `Symbol.hasInstance` to check a brand symbol
16
+ * (`Symbol.for('silgi.error.brand.v1')`) stamped on `SilgiError.prototype`.
17
+ * Because `Symbol.for` resolves through the global symbol registry shared
18
+ * across all V8 realms (worker threads, `node:vm` contexts), `instanceof
19
+ * SilgiError` works even when the `SilgiError` class used to construct the
20
+ * error originated in a different realm.
21
+ *
22
+ * ### Subclassing
23
+ * User-defined subclasses are fully supported:
24
+ * ```ts
25
+ * class AuthError extends SilgiError<'UNAUTHORIZED'> {
26
+ * constructor() { super('UNAUTHORIZED', { status: 401 }) }
27
+ * }
28
+ * ```
29
+ * The brand is inherited through the prototype chain automatically — no
30
+ * extra steps are required in the subclass constructor.
31
+ *
32
+ * @category Errors
4
33
  */
34
+ /** Well-known HTTP error codes with their default status and message. */
5
35
  declare const COMMON_ERRORS: Readonly<{
6
36
  readonly BAD_REQUEST: {
7
37
  readonly status: 400;
@@ -73,6 +103,7 @@ declare const COMMON_ERRORS: Readonly<{
73
103
  };
74
104
  }>;
75
105
  type SilgiErrorCode = keyof typeof COMMON_ERRORS | (string & {});
106
+ /** Options passed to the {@link SilgiError} constructor. */
76
107
  interface SilgiErrorOptions<TData = unknown> {
77
108
  status?: number;
78
109
  message?: string;
@@ -80,6 +111,7 @@ interface SilgiErrorOptions<TData = unknown> {
80
111
  cause?: unknown;
81
112
  defined?: boolean;
82
113
  }
114
+ /** Wire-format representation of a {@link SilgiError}, safe to JSON-stringify and transmit to clients. */
83
115
  interface SilgiErrorJSON<TCode extends string = string, TData = unknown> {
84
116
  defined: boolean;
85
117
  code: TCode;
@@ -87,18 +119,167 @@ interface SilgiErrorJSON<TCode extends string = string, TData = unknown> {
87
119
  message: string;
88
120
  data: TData;
89
121
  }
122
+ /**
123
+ * A typed, serializable RPC error.
124
+ *
125
+ * @typeParam TCode - String literal type for the error code.
126
+ * @typeParam TData - Shape of the optional structured data payload.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * throw new SilgiError('NOT_FOUND', { message: 'User 42 not found' })
131
+ * ```
132
+ *
133
+ * @example Checking for Silgi errors in a catch block
134
+ * ```ts
135
+ * import { isSilgiError } from 'silgi'
136
+ *
137
+ * try {
138
+ * await client.users.get({ id: '42' })
139
+ * } catch (e) {
140
+ * if (isSilgiError(e)) {
141
+ * console.error(e.code, e.status)
142
+ * }
143
+ * }
144
+ * ```
145
+ *
146
+ * @see {@link isSilgiError} Preferred type-guard over `instanceof` for cross-realm safety.
147
+ * @see {@link toSilgiError} Wraps any unknown error into a `SilgiError`.
148
+ * @see {@link fromSilgiErrorJSON} Reconstructs a `SilgiError` from a wire-format JSON object.
149
+ *
150
+ * @remarks
151
+ * **Do not mutate the prototype brand.** Removing or overwriting
152
+ * `SilgiError.prototype[Symbol.for('silgi.error.brand.v1')]` will break
153
+ * cross-realm detection for all instances in the current realm.
154
+ *
155
+ * **Subclass note.** If a subclass overrides the constructor and somehow
156
+ * replaces `Object.getPrototypeOf(this)` before construction completes,
157
+ * the inherited brand may be severed. This is an extremely unusual pattern
158
+ * and is not supported.
159
+ *
160
+ * @category Errors
161
+ */
90
162
  declare class SilgiError<TCode extends string = string, TData = unknown> extends Error {
163
+ /** Machine-readable error code (e.g. `'NOT_FOUND'`). */
91
164
  readonly code: TCode;
165
+ /** HTTP status code associated with this error. */
92
166
  readonly status: number;
167
+ /** Optional structured payload carrying additional error context. */
93
168
  readonly data: TData;
169
+ /**
170
+ * `true` when this error was declared via `$errors()` and is safe to
171
+ * expose verbatim to clients; `false` for internal faults.
172
+ */
94
173
  readonly defined: boolean;
174
+ /**
175
+ * @param code - Machine-readable error code. Well-known codes (e.g.
176
+ * `'NOT_FOUND'`, `'UNAUTHORIZED'`) resolve default `status` and
177
+ * `message` values automatically.
178
+ * @param options - Override `status`, `message`, `data`, `cause`, and
179
+ * `defined` fields.
180
+ */
95
181
  constructor(code: TCode, options?: SilgiErrorOptions<TData>);
182
+ /**
183
+ * Serialize this error to a plain object suitable for JSON transmission.
184
+ *
185
+ * @remarks
186
+ * The brand symbol (`Symbol.for('silgi.error.brand.v1')`) is
187
+ * non-enumerable and lives on the prototype, so it is never included
188
+ * in the output of `toJSON()` or `JSON.stringify()`.
189
+ *
190
+ * @returns A {@link SilgiErrorJSON} with `defined`, `code`, `status`,
191
+ * `message`, and `data` fields — and nothing else.
192
+ */
96
193
  toJSON(): SilgiErrorJSON<TCode, TData>;
194
+ /**
195
+ * Custom `instanceof` check that works across V8 realms.
196
+ *
197
+ * @remarks
198
+ * Checks for the presence of `Symbol.for('silgi.error.brand.v1')` on
199
+ * the candidate value's prototype chain. Because `Symbol.for` resolves
200
+ * through the global symbol registry (shared across all realms), this
201
+ * correctly identifies `SilgiError` instances that were constructed in a
202
+ * worker thread or `node:vm` context.
203
+ *
204
+ * This is an O(1) property lookup — no prototype walk is performed.
205
+ *
206
+ * @param instance - The value to test.
207
+ * @returns `true` if `instance` carries the Silgi error brand.
208
+ */
97
209
  static [Symbol.hasInstance](instance: unknown): boolean;
98
210
  }
211
+ /**
212
+ * Type guard that returns `true` when `e` is a {@link SilgiError}.
213
+ *
214
+ * @remarks
215
+ * Prefer this function over `instanceof SilgiError` in application code.
216
+ * The brand check is realm-transparent and does not depend on the
217
+ * `SilgiError` class reference being the exact same object across module
218
+ * boundaries. It is also marginally faster than `instanceof` because it
219
+ * skips the `[Symbol.hasInstance]` dispatch and reads the brand directly.
220
+ *
221
+ * @param e - Any value — typically a caught `unknown` error.
222
+ * @returns `true` if `e` carries the `Symbol.for('silgi.error.brand.v1')` brand.
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * catch (e) {
227
+ * if (isSilgiError(e)) {
228
+ * // e is SilgiError — access e.code, e.status, e.data safely
229
+ * }
230
+ * }
231
+ * ```
232
+ *
233
+ * @see {@link SilgiError}
234
+ * @category Errors
235
+ */
236
+ declare function isSilgiError(e: unknown): e is SilgiError;
237
+ /**
238
+ * Returns `true` when `error` is a {@link SilgiError} that was explicitly
239
+ * declared with `$errors()` (i.e. `defined === true`).
240
+ *
241
+ * @remarks
242
+ * Defined errors are safe to expose verbatim to clients; undefined errors
243
+ * should be redacted to a generic `INTERNAL_SERVER_ERROR` message.
244
+ *
245
+ * @param error - Any value.
246
+ * @returns A type predicate narrowing to `TError & SilgiError & \{ defined: true \}`.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * if (isDefinedError(e)) {
251
+ * // e.defined === true — forward error details to the client
252
+ * }
253
+ * ```
254
+ *
255
+ * @see {@link isSilgiError}
256
+ * @category Errors
257
+ */
99
258
  declare function isDefinedError<TError>(error: TError): error is TError & SilgiError & {
100
259
  defined: true;
101
260
  };
261
+ /**
262
+ * Coerce any caught value into a {@link SilgiError}.
263
+ *
264
+ * @remarks
265
+ * If `error` is already a `SilgiError` it is returned unchanged.
266
+ * Any other value (plain `Error`, string, etc.) is wrapped in an
267
+ * `INTERNAL_SERVER_ERROR` with the original value preserved as `cause`.
268
+ * The wrapper message is intentionally generic to avoid leaking internal
269
+ * details to clients.
270
+ *
271
+ * @param error - Any value — typically the argument of a `catch` block.
272
+ * @returns The original `SilgiError`, or a new `SilgiError('INTERNAL_SERVER_ERROR')`.
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * const e = toSilgiError(err)
277
+ * res.status(e.status).json(e.toJSON())
278
+ * ```
279
+ *
280
+ * @see {@link isSilgiError}
281
+ * @category Errors
282
+ */
102
283
  declare function toSilgiError(error: unknown): SilgiError;
103
284
  //#endregion
104
- export { SilgiError, SilgiErrorCode, SilgiErrorJSON, SilgiErrorOptions, isDefinedError, toSilgiError };
285
+ export { SilgiError, SilgiErrorCode, SilgiErrorJSON, SilgiErrorOptions, isDefinedError, isSilgiError, toSilgiError };
@@ -1,7 +1,37 @@
1
1
  //#region src/core/error.ts
2
2
  /**
3
- * SilgiError — unified RPC error with cross-realm instanceof.
3
+ * SilgiError — unified RPC error type for the Silgi framework.
4
+ *
5
+ * @remarks
6
+ * Every error thrown through the Silgi request pipeline is either a
7
+ * `SilgiError` or is wrapped into one by {@link toSilgiError} before it
8
+ * reaches the wire. The class carries an HTTP `status`, a machine-readable
9
+ * `code`, optional structured `data`, and a `defined` flag that signals
10
+ * whether the error was deliberately declared with `$errors()` (safe to
11
+ * expose verbatim to clients) or is an internal fault that should be
12
+ * redacted.
13
+ *
14
+ * ### Cross-realm `instanceof`
15
+ * `SilgiError` overrides `Symbol.hasInstance` to check a brand symbol
16
+ * (`Symbol.for('silgi.error.brand.v1')`) stamped on `SilgiError.prototype`.
17
+ * Because `Symbol.for` resolves through the global symbol registry shared
18
+ * across all V8 realms (worker threads, `node:vm` contexts), `instanceof
19
+ * SilgiError` works even when the `SilgiError` class used to construct the
20
+ * error originated in a different realm.
21
+ *
22
+ * ### Subclassing
23
+ * User-defined subclasses are fully supported:
24
+ * ```ts
25
+ * class AuthError extends SilgiError<'UNAUTHORIZED'> {
26
+ * constructor() { super('UNAUTHORIZED', { status: 401 }) }
27
+ * }
28
+ * ```
29
+ * The brand is inherited through the prototype chain automatically — no
30
+ * extra steps are required in the subclass constructor.
31
+ *
32
+ * @category Errors
4
33
  */
34
+ /** Well-known HTTP error codes with their default status and message. */
5
35
  const COMMON_ERRORS = /* @__PURE__ */ Object.freeze({
6
36
  BAD_REQUEST: {
7
37
  status: 400,
@@ -72,13 +102,81 @@ const COMMON_ERRORS = /* @__PURE__ */ Object.freeze({
72
102
  message: "Gateway Timeout"
73
103
  }
74
104
  });
75
- const REGISTRY_KEY = Symbol.for("silgi.error.registry");
76
- const registry = globalThis[REGISTRY_KEY] ??= /* @__PURE__ */ new WeakSet();
105
+ /**
106
+ * Non-enumerable brand symbol stamped on `SilgiError.prototype`.
107
+ *
108
+ * Using `Symbol.for` ensures the key is resolved from the global symbol
109
+ * registry, which is shared across all V8 realms (worker threads,
110
+ * `node:vm` contexts). This makes cross-realm `instanceof` and
111
+ * {@link isSilgiError} checks reliable without any module-level WeakSet
112
+ * or `globalThis` state.
113
+ *
114
+ * The `.brand.v1` suffix prevents accidental collision with user code that
115
+ * might use the shorter `Symbol.for('silgi.error')` for unrelated purposes,
116
+ * and reserves the `v1` slot for a future migration if brand semantics change.
117
+ *
118
+ * @internal
119
+ */
120
+ const BRAND = Symbol.for("silgi.error.brand.v1");
121
+ /**
122
+ * A typed, serializable RPC error.
123
+ *
124
+ * @typeParam TCode - String literal type for the error code.
125
+ * @typeParam TData - Shape of the optional structured data payload.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * throw new SilgiError('NOT_FOUND', { message: 'User 42 not found' })
130
+ * ```
131
+ *
132
+ * @example Checking for Silgi errors in a catch block
133
+ * ```ts
134
+ * import { isSilgiError } from 'silgi'
135
+ *
136
+ * try {
137
+ * await client.users.get({ id: '42' })
138
+ * } catch (e) {
139
+ * if (isSilgiError(e)) {
140
+ * console.error(e.code, e.status)
141
+ * }
142
+ * }
143
+ * ```
144
+ *
145
+ * @see {@link isSilgiError} Preferred type-guard over `instanceof` for cross-realm safety.
146
+ * @see {@link toSilgiError} Wraps any unknown error into a `SilgiError`.
147
+ * @see {@link fromSilgiErrorJSON} Reconstructs a `SilgiError` from a wire-format JSON object.
148
+ *
149
+ * @remarks
150
+ * **Do not mutate the prototype brand.** Removing or overwriting
151
+ * `SilgiError.prototype[Symbol.for('silgi.error.brand.v1')]` will break
152
+ * cross-realm detection for all instances in the current realm.
153
+ *
154
+ * **Subclass note.** If a subclass overrides the constructor and somehow
155
+ * replaces `Object.getPrototypeOf(this)` before construction completes,
156
+ * the inherited brand may be severed. This is an extremely unusual pattern
157
+ * and is not supported.
158
+ *
159
+ * @category Errors
160
+ */
77
161
  var SilgiError = class extends Error {
162
+ /** Machine-readable error code (e.g. `'NOT_FOUND'`). */
78
163
  code;
164
+ /** HTTP status code associated with this error. */
79
165
  status;
166
+ /** Optional structured payload carrying additional error context. */
80
167
  data;
168
+ /**
169
+ * `true` when this error was declared via `$errors()` and is safe to
170
+ * expose verbatim to clients; `false` for internal faults.
171
+ */
81
172
  defined;
173
+ /**
174
+ * @param code - Machine-readable error code. Well-known codes (e.g.
175
+ * `'NOT_FOUND'`, `'UNAUTHORIZED'`) resolve default `status` and
176
+ * `message` values automatically.
177
+ * @param options - Override `status`, `message`, `data`, `cause`, and
178
+ * `defined` fields.
179
+ */
82
180
  constructor(code, options = {}) {
83
181
  const defaults = COMMON_ERRORS[code];
84
182
  const message = options.message ?? defaults?.message ?? code;
@@ -89,6 +187,17 @@ var SilgiError = class extends Error {
89
187
  this.defined = options.defined ?? false;
90
188
  this.name = "SilgiError";
91
189
  }
190
+ /**
191
+ * Serialize this error to a plain object suitable for JSON transmission.
192
+ *
193
+ * @remarks
194
+ * The brand symbol (`Symbol.for('silgi.error.brand.v1')`) is
195
+ * non-enumerable and lives on the prototype, so it is never included
196
+ * in the output of `toJSON()` or `JSON.stringify()`.
197
+ *
198
+ * @returns A {@link SilgiErrorJSON} with `defined`, `code`, `status`,
199
+ * `message`, and `data` fields — and nothing else.
200
+ */
92
201
  toJSON() {
93
202
  return {
94
203
  defined: this.defined,
@@ -98,35 +207,169 @@ var SilgiError = class extends Error {
98
207
  data: this.data
99
208
  };
100
209
  }
101
- static {
102
- registry.add(this);
103
- }
210
+ /**
211
+ * Custom `instanceof` check that works across V8 realms.
212
+ *
213
+ * @remarks
214
+ * Checks for the presence of `Symbol.for('silgi.error.brand.v1')` on
215
+ * the candidate value's prototype chain. Because `Symbol.for` resolves
216
+ * through the global symbol registry (shared across all realms), this
217
+ * correctly identifies `SilgiError` instances that were constructed in a
218
+ * worker thread or `node:vm` context.
219
+ *
220
+ * This is an O(1) property lookup — no prototype walk is performed.
221
+ *
222
+ * @param instance - The value to test.
223
+ * @returns `true` if `instance` carries the Silgi error brand.
224
+ */
104
225
  static [Symbol.hasInstance](instance) {
105
- if (typeof instance !== "object" || instance === null) return false;
106
- let proto = Object.getPrototypeOf(instance);
107
- while (proto) {
108
- if (proto.constructor && registry.has(proto.constructor)) return true;
109
- proto = Object.getPrototypeOf(proto);
110
- }
111
- return false;
226
+ return typeof instance === "object" && instance !== null && instance[BRAND] === true;
112
227
  }
113
228
  };
229
+ Reflect.defineProperty(SilgiError.prototype, BRAND, {
230
+ value: true,
231
+ enumerable: false,
232
+ writable: false,
233
+ configurable: false
234
+ });
235
+ /**
236
+ * Type guard that returns `true` when `e` is a {@link SilgiError}.
237
+ *
238
+ * @remarks
239
+ * Prefer this function over `instanceof SilgiError` in application code.
240
+ * The brand check is realm-transparent and does not depend on the
241
+ * `SilgiError` class reference being the exact same object across module
242
+ * boundaries. It is also marginally faster than `instanceof` because it
243
+ * skips the `[Symbol.hasInstance]` dispatch and reads the brand directly.
244
+ *
245
+ * @param e - Any value — typically a caught `unknown` error.
246
+ * @returns `true` if `e` carries the `Symbol.for('silgi.error.brand.v1')` brand.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * catch (e) {
251
+ * if (isSilgiError(e)) {
252
+ * // e is SilgiError — access e.code, e.status, e.data safely
253
+ * }
254
+ * }
255
+ * ```
256
+ *
257
+ * @see {@link SilgiError}
258
+ * @category Errors
259
+ */
260
+ function isSilgiError(e) {
261
+ return typeof e === "object" && e !== null && e[BRAND] === true;
262
+ }
263
+ /**
264
+ * Returns `true` when `error` is a {@link SilgiError} that was explicitly
265
+ * declared with `$errors()` (i.e. `defined === true`).
266
+ *
267
+ * @remarks
268
+ * Defined errors are safe to expose verbatim to clients; undefined errors
269
+ * should be redacted to a generic `INTERNAL_SERVER_ERROR` message.
270
+ *
271
+ * @param error - Any value.
272
+ * @returns A type predicate narrowing to `TError & SilgiError & \{ defined: true \}`.
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * if (isDefinedError(e)) {
277
+ * // e.defined === true — forward error details to the client
278
+ * }
279
+ * ```
280
+ *
281
+ * @see {@link isSilgiError}
282
+ * @category Errors
283
+ */
114
284
  function isDefinedError(error) {
115
- return error instanceof SilgiError && error.defined === true;
285
+ return isSilgiError(error) && error.defined === true;
116
286
  }
287
+ /**
288
+ * Coerce any caught value into a {@link SilgiError}.
289
+ *
290
+ * @remarks
291
+ * If `error` is already a `SilgiError` it is returned unchanged.
292
+ * Any other value (plain `Error`, string, etc.) is wrapped in an
293
+ * `INTERNAL_SERVER_ERROR` with the original value preserved as `cause`.
294
+ * The wrapper message is intentionally generic to avoid leaking internal
295
+ * details to clients.
296
+ *
297
+ * @param error - Any value — typically the argument of a `catch` block.
298
+ * @returns The original `SilgiError`, or a new `SilgiError('INTERNAL_SERVER_ERROR')`.
299
+ *
300
+ * @example
301
+ * ```ts
302
+ * const e = toSilgiError(err)
303
+ * res.status(e.status).json(e.toJSON())
304
+ * ```
305
+ *
306
+ * @see {@link isSilgiError}
307
+ * @category Errors
308
+ */
117
309
  function toSilgiError(error) {
118
- if (error instanceof SilgiError) return error;
310
+ if (isSilgiError(error)) return error;
119
311
  return new SilgiError("INTERNAL_SERVER_ERROR", {
120
312
  message: "Internal server error",
121
313
  cause: error
122
314
  });
123
315
  }
316
+ /**
317
+ * Returns `true` when `status` represents an HTTP error (4xx or 5xx).
318
+ *
319
+ * @param status - An HTTP status code.
320
+ * @returns `true` when `status >= 400`.
321
+ *
322
+ * @example
323
+ * ```ts
324
+ * if (isErrorStatus(response.status)) handleError(response)
325
+ * ```
326
+ *
327
+ * @category Errors
328
+ */
124
329
  function isErrorStatus(status) {
125
330
  return status >= 400;
126
331
  }
332
+ /**
333
+ * Type guard that returns `true` when `json` has the shape of a
334
+ * {@link SilgiErrorJSON} wire object.
335
+ *
336
+ * @param json - Any decoded JSON value.
337
+ * @returns `true` when `json` has a string `code` and a numeric `status`.
338
+ *
339
+ * @example
340
+ * ```ts
341
+ * const body = await response.json()
342
+ * if (isSilgiErrorJSON(body)) {
343
+ * throw fromSilgiErrorJSON(body)
344
+ * }
345
+ * ```
346
+ *
347
+ * @see {@link fromSilgiErrorJSON}
348
+ * @category Errors
349
+ */
127
350
  function isSilgiErrorJSON(json) {
128
351
  return typeof json === "object" && json !== null && "code" in json && "status" in json && typeof json.code === "string";
129
352
  }
353
+ /**
354
+ * Reconstruct a {@link SilgiError} from a {@link SilgiErrorJSON} wire object.
355
+ *
356
+ * @remarks
357
+ * Typically used on the client side after receiving an error response.
358
+ * The reconstructed error carries the brand and passes `isSilgiError()`.
359
+ *
360
+ * @param json - A validated {@link SilgiErrorJSON} object (use
361
+ * {@link isSilgiErrorJSON} to validate before calling this function).
362
+ * @returns A fully-formed `SilgiError` instance.
363
+ *
364
+ * @example
365
+ * ```ts
366
+ * const body = await res.json()
367
+ * if (isSilgiErrorJSON(body)) throw fromSilgiErrorJSON(body)
368
+ * ```
369
+ *
370
+ * @see {@link isSilgiErrorJSON}
371
+ * @category Errors
372
+ */
130
373
  function fromSilgiErrorJSON(json) {
131
374
  return new SilgiError(json.code, {
132
375
  status: json.status,
@@ -136,4 +379,4 @@ function fromSilgiErrorJSON(json) {
136
379
  });
137
380
  }
138
381
  //#endregion
139
- export { SilgiError, fromSilgiErrorJSON, isDefinedError, isErrorStatus, isSilgiErrorJSON, toSilgiError };
382
+ export { SilgiError, fromSilgiErrorJSON, isDefinedError, isErrorStatus, isSilgiError, isSilgiErrorJSON, toSilgiError };
@@ -1,14 +1,28 @@
1
1
  import { AnalyticsOptions } from "../plugins/analytics/types.mjs";
2
+ import { SchemaRegistry } from "./schema-converter.mjs";
2
3
  import { ScalarOptions } from "../scalar.mjs";
4
+ import { SilgiHooks } from "../silgi.mjs";
3
5
  import { Hookable } from "hookable";
4
6
 
5
7
  //#region src/core/handler.d.ts
6
8
  type FetchHandler = (request: Request) => Response | Promise<Response>;
7
9
  interface WrapHandlerOptions {
8
- analytics?: boolean | AnalyticsOptions;
10
+ analytics?: AnalyticsOptions;
9
11
  scalar?: boolean | ScalarOptions;
10
12
  /** URL path prefix for the handler (e.g. "/api"). Requests not matching this prefix return 404. */
11
13
  basePath?: string;
14
+ /**
15
+ * Schema registry for OpenAPI / analytics schema conversion. Built from
16
+ * `schemaConverters` in the silgi instance config — do not set manually.
17
+ * @internal
18
+ */
19
+ schemaRegistry?: SchemaRegistry;
20
+ /**
21
+ * Hookable instance — threaded so `wrapWithAnalytics` can register
22
+ * lifecycle listeners on `request:prepare` / `response:finalize`.
23
+ * @internal
24
+ */
25
+ hooks?: Hookable<SilgiHooks>;
12
26
  }
13
27
  //#endregion
14
28
  export { FetchHandler, WrapHandlerOptions };