stitchkit 0.41.0 → 0.43.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.
Files changed (66) hide show
  1. package/README.md +22 -5
  2. package/dist/cli.js +2 -2
  3. package/dist/contract/errors-factory.d.ts +88 -15
  4. package/dist/contract/errors-factory.d.ts.map +1 -1
  5. package/dist/contract/errors.d.ts +4 -4
  6. package/dist/contract/errors.d.ts.map +1 -1
  7. package/dist/contract/factory.d.ts +29 -2
  8. package/dist/contract/factory.d.ts.map +1 -1
  9. package/dist/contract/index.d.ts +2 -2
  10. package/dist/contract/index.d.ts.map +1 -1
  11. package/dist/contract/index.js +2 -2
  12. package/dist/{index-dnkefke9.js → index-310bfer5.js} +1 -1
  13. package/dist/{index-4e1c0hsw.js → index-esqmem78.js} +101 -67
  14. package/dist/{index-sc67e454.js → index-gex6gxhe.js} +7 -4
  15. package/dist/{index-ncqqn1bc.js → index-h2wdcsby.js} +95 -17
  16. package/dist/{index-xax049k6.js → index-r6czv7yt.js} +58 -22
  17. package/dist/{index-809wc1tt.js → index-s4qsmgwe.js} +4 -1
  18. package/dist/index.js +2 -2
  19. package/dist/internal/typed.d.ts +8 -0
  20. package/dist/internal/typed.d.ts.map +1 -1
  21. package/dist/node.js +2 -2
  22. package/dist/observability/audit.d.ts +27 -15
  23. package/dist/observability/audit.d.ts.map +1 -1
  24. package/dist/observability/event.d.ts +2 -2
  25. package/dist/observability/index.d.ts +4 -4
  26. package/dist/observability/index.d.ts.map +1 -1
  27. package/dist/observability/index.js +70 -59
  28. package/dist/react.js +1 -1
  29. package/dist/server/create.d.ts.map +1 -1
  30. package/dist/server/error-hook.d.ts +8 -4
  31. package/dist/server/error-hook.d.ts.map +1 -1
  32. package/dist/server/index.js +7 -6
  33. package/dist/server/logger.d.ts +0 -1
  34. package/dist/server/logger.d.ts.map +1 -1
  35. package/dist/server/types.d.ts +8 -13
  36. package/dist/server/types.d.ts.map +1 -1
  37. package/dist/tools/agent.d.ts +1 -1
  38. package/dist/tools/agent.d.ts.map +1 -1
  39. package/dist/tools/flatten-join.d.ts.map +1 -1
  40. package/dist/tools/list-names.d.ts +8 -6
  41. package/dist/tools/list-names.d.ts.map +1 -1
  42. package/dist/tools/manifest.d.ts +13 -5
  43. package/dist/tools/manifest.d.ts.map +1 -1
  44. package/dist/tools/mcp-handler.d.ts +5 -4
  45. package/dist/tools/mcp-handler.d.ts.map +1 -1
  46. package/dist/tools/mcp-stdio.d.ts +3 -2
  47. package/dist/tools/mcp-stdio.d.ts.map +1 -1
  48. package/dist/tools/mcp.d.ts +49 -9
  49. package/dist/tools/mcp.d.ts.map +1 -1
  50. package/dist/tools/native-mcp.d.ts +4 -18
  51. package/dist/tools/native-mcp.d.ts.map +1 -1
  52. package/dist/tools/runtime-tool.d.ts +32 -1
  53. package/dist/tools/runtime-tool.d.ts.map +1 -1
  54. package/dist/tools/surface.d.ts +37 -0
  55. package/dist/tools/surface.d.ts.map +1 -0
  56. package/dist/tools/toolkit.d.ts +4 -4
  57. package/dist/tools/toolkit.d.ts.map +1 -1
  58. package/dist/tools/transports.d.ts +11 -9
  59. package/dist/tools/transports.d.ts.map +1 -1
  60. package/dist/tools/view-file.d.ts +3 -3
  61. package/dist/tools.d.ts +6 -6
  62. package/dist/tools.d.ts.map +1 -1
  63. package/dist/tools.js +330 -155
  64. package/llms-full.txt +524 -131
  65. package/llms.txt +1 -0
  66. package/package.json +2 -2
package/README.md CHANGED
@@ -51,6 +51,17 @@ supported via `stitchkit/node`.
51
51
 
52
52
  ## Install
53
53
 
54
+ Start a production-shaped Next.js, Stitchkit and PostgreSQL application:
55
+
56
+ ```bash
57
+ bun create stitchkit my-app
58
+ cd my-app
59
+ # Point DATABASE_URL in .env at your PostgreSQL database.
60
+ bun run dev
61
+ ```
62
+
63
+ To add Stitchkit to an existing project instead:
64
+
54
65
  ```bash
55
66
  bun add stitchkit # Bun
56
67
  npm install stitchkit # Node
@@ -348,11 +359,17 @@ never touches MCP never installs the MCP SDK. → [ADR 0011](./docs/decisions/00
348
359
  The framework stays focused and inspectable: explicit adapters, no generated
349
360
  application code and no framework build step in your app.
350
361
 
351
- ## Example
352
-
353
- A complete runnable app lives in [`packages/starter`](./packages/starter) a
354
- notes CRUD with a contract, a typed client, `react-query-kit` hooks and a
355
- Socket.IO live-reload.
362
+ ## Official starter
363
+
364
+ `bun create stitchkit my-app` generates the canonical application: separate
365
+ Next.js and Bun API processes, Prisma/PostgreSQL, shared Zod contracts, typed
366
+ HTTP/React Query clients, Socket.IO cache updates, OpenAPI, MCP, CLI and a full
367
+ UI catalogue. Its only source is
368
+ [`packages/create-stitchkit/template`](./packages/create-stitchkit/template).
369
+ The application owns its Prisma schema and migrations while PostgreSQL remains
370
+ external infrastructure configured through `DATABASE_URL`.
371
+ The template owns a committed Bun lockfile and an explicit Stitchkit catalog
372
+ range, so framework and scaffolder releases advance independently.
356
373
 
357
374
  ## Documentation — two roads
358
375
 
package/dist/cli.js CHANGED
@@ -4,10 +4,10 @@ import {
4
4
  emitResult,
5
5
  parseCliArgs,
6
6
  pollUntilDone
7
- } from "./index-ncqqn1bc.js";
7
+ } from "./index-h2wdcsby.js";
8
8
  import"./index-frfyw9fa.js";
9
9
  import"./index-x3fcszf8.js";
10
- import"./index-sc67e454.js";
10
+ import"./index-gex6gxhe.js";
11
11
  export {
12
12
  pollUntilDone,
13
13
  parseCliArgs,
@@ -1,18 +1,91 @@
1
- /** A typed thrower — throws an `AppError` with the declared code and status. */
2
- export type ErrorThrower = (message?: string, details?: Record<string, unknown>, hint?: string) => never;
3
- /** The handle `defineErrors` returns. `TDef` maps `CODE HTTP status`. */
4
- export interface DefinedErrors<TDef extends Record<string, number>> {
5
- /** One thrower per code — `errors.CODE(message?)` throws the matching `AppError`. */
6
- errors: {
7
- [K in keyof TDef]: ErrorThrower;
1
+ /**
2
+ * Declare an application's domain errors once. Each definition owns its HTTP
3
+ * status and optional Zod schema for structured details; generated factories
4
+ * construct (but do not throw) branded `AppError` instances.
5
+ *
6
+ * ```ts
7
+ * export const appErrors = defineErrors({
8
+ * SESSION_NOT_FOUND: { status: 404 },
9
+ * QUOTA_EXCEEDED: {
10
+ * status: 429,
11
+ * details: z.object({ retryAfterSeconds: z.number().int().positive() }),
12
+ * },
13
+ * })
14
+ *
15
+ * throw appErrors.errors.SESSION_NOT_FOUND({ message: 'No such session' })
16
+ * throw appErrors.errors.QUOTA_EXCEEDED({
17
+ * details: { retryAfterSeconds: 30 },
18
+ * hint: 'Wait for the current window to expire',
19
+ * })
20
+ * ```
21
+ */
22
+ import { z } from 'zod';
23
+ import { AppError } from './errors';
24
+ /** Supported structured-details schemas: a required or optional Zod object. */
25
+ export type ErrorDetailsSchema = z.ZodObject | z.ZodOptional<z.ZodObject>;
26
+ /** One domain error definition. Omitting `details` forbids structured details. */
27
+ export type ErrorDefinition = {
28
+ readonly status: number;
29
+ readonly details?: never;
30
+ } | {
31
+ readonly status: number;
32
+ readonly details: ErrorDetailsSchema;
33
+ };
34
+ export type ErrorDefinitions = Record<string, ErrorDefinition>;
35
+ /** Parsed details retained by the constructed `AppError`. */
36
+ export type ErrorDetailsOutput<TDefinition extends ErrorDefinition> = TDefinition extends {
37
+ details: infer TSchema extends ErrorDetailsSchema;
38
+ } ? Extract<z.output<TSchema>, Record<string, unknown> | undefined> : undefined;
39
+ /** AppError instance with required details refined when its schema is required. */
40
+ export type DefinedAppError<TCode extends string, TDefinition extends ErrorDefinition> = AppError<TCode, ErrorDetailsOutput<TDefinition>> & (TDefinition extends {
41
+ details: infer TSchema extends ErrorDetailsSchema;
42
+ } ? undefined extends z.output<TSchema> ? object : {
43
+ readonly details: ErrorDetailsOutput<TDefinition>;
44
+ } : object);
45
+ /**
46
+ * Factory arguments inferred from the definition: details are forbidden,
47
+ * required or optional according to the declared schema.
48
+ */
49
+ export type ErrorFactoryArguments<TDefinition extends ErrorDefinition> = TDefinition extends {
50
+ details: infer TSchema extends ErrorDetailsSchema;
51
+ } ? undefined extends z.input<TSchema> ? [
52
+ options?: {
53
+ message?: string;
54
+ details?: Exclude<z.input<TSchema>, undefined>;
55
+ hint?: string;
56
+ }
57
+ ] : [
58
+ options: {
59
+ message?: string;
60
+ details: z.input<TSchema>;
61
+ hint?: string;
62
+ }
63
+ ] : [options?: {
64
+ message?: string;
65
+ details?: never;
66
+ hint?: string;
67
+ }];
68
+ /** Typed constructor for one declared domain error code. */
69
+ export type ErrorFactory<TCode extends string, TDefinition extends ErrorDefinition> = (...args: ErrorFactoryArguments<TDefinition>) => DefinedAppError<TCode, TDefinition>;
70
+ export type ErrorFactories<TDefinitions extends ErrorDefinitions> = {
71
+ readonly [TCode in keyof TDefinitions]: ErrorFactory<TCode & string, TDefinitions[TCode]>;
72
+ };
73
+ export type FrozenErrorDefinitions<TDefinitions extends ErrorDefinitions> = {
74
+ readonly [TCode in keyof TDefinitions]: Readonly<TDefinitions[TCode]>;
75
+ };
76
+ /** The immutable handle returned by `defineErrors`. */
77
+ export interface DefinedErrors<TDefinitions extends ErrorDefinitions> {
78
+ /** One typed `AppError` constructor per code. The caller chooses when to throw. */
79
+ readonly errors: ErrorFactories<TDefinitions>;
80
+ /** Code literals for client-side matching without magic strings. */
81
+ readonly codes: {
82
+ readonly [TCode in keyof TDefinitions]: TCode & string;
8
83
  };
9
- /** The code literals `codes.CODE === 'CODE'`, for client-side matching. */
10
- codes: {
11
- readonly [K in keyof TDef]: K;
12
- };
13
- /** Type guard — is `code` one of this app's declared codes? */
14
- isCode: (code: string) => code is keyof TDef & string;
84
+ /** Read-only definitions; status and details schemas remain one source of truth. */
85
+ readonly definitions: FrozenErrorDefinitions<TDefinitions>;
86
+ /** Type guard is `code` one of this application's declared codes? */
87
+ readonly isCode: (code: string) => code is Extract<keyof TDefinitions, string>;
15
88
  }
16
- /** Declare a set of domain error codes (`{ CODE: httpStatus }`). */
17
- export declare function defineErrors<const TDef extends Record<string, number>>(defs: TDef): DefinedErrors<TDef>;
89
+ /** Declare an immutable, Zod-first domain error vocabulary. */
90
+ export declare function defineErrors<const TDefinitions extends ErrorDefinitions>(source: TDefinitions): DefinedErrors<TDefinitions>;
18
91
  //# sourceMappingURL=errors-factory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors-factory.d.ts","sourceRoot":"","sources":["../../src/contract/errors-factory.ts"],"names":[],"mappings":"AA8BA,gFAAgF;AAChF,MAAM,MAAM,YAAY,GAAG,CACzB,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,IAAI,CAAC,EAAE,MAAM,KACV,KAAK,CAAC;AAEX,2EAA2E;AAC3E,MAAM,WAAW,aAAa,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAChE,qFAAqF;IACrF,MAAM,EAAE;SAAG,CAAC,IAAI,MAAM,IAAI,GAAG,YAAY;KAAE,CAAC;IAC5C,6EAA6E;IAC7E,KAAK,EAAE;QAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,IAAI,GAAG,CAAC;KAAE,CAAC;IACzC,+DAA+D;IAC/D,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,IAAI,MAAM,IAAI,GAAG,MAAM,CAAC;CACvD;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,KAAK,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpE,IAAI,EAAE,IAAI,GACT,aAAa,CAAC,IAAI,CAAC,CAgBrB"}
1
+ {"version":3,"file":"errors-factory.d.ts","sourceRoot":"","sources":["../../src/contract/errors-factory.ts"],"names":[],"mappings":"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"}
@@ -21,12 +21,12 @@ export interface ErrorEnvelope {
21
21
  * structured `details` and a `hint`. `toJSON()` renders the public error
22
22
  * envelope. Throw it directly, or via the typed helpers below.
23
23
  */
24
- export declare class AppError extends Error {
25
- readonly code: string;
24
+ export declare class AppError<TCode extends string = string, TDetails extends Record<string, unknown> | undefined = Record<string, unknown> | undefined> extends Error {
25
+ readonly code: TCode;
26
26
  readonly status: number;
27
- readonly details?: Record<string, unknown> | undefined;
27
+ readonly details?: TDetails | undefined;
28
28
  readonly hint?: string | undefined;
29
- constructor(code: string, message?: string, status?: number, details?: Record<string, unknown> | undefined, hint?: string | undefined);
29
+ constructor(code: TCode, message?: string, status?: number, details?: TDetails | undefined, hint?: string | undefined);
30
30
  static is(err: unknown): err is AppError;
31
31
  toJSON(): ErrorEnvelope;
32
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/contract/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE;QACL,2CAA2C;QAC3C,IAAI,EAAE,MAAM,CAAC;QACb,8BAA8B;QAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,gCAAgC;QAChC,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,0BAA0B;QAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAcD;;;;GAIG;AACH,qBAAa,QAAS,SAAQ,KAAK;aAEf,IAAI,EAAE,MAAM;aAEZ,MAAM,EAAE,MAAM;aACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;aACjC,IAAI,CAAC,EAAE,MAAM;IAL/B,YACkB,IAAI,EAAE,MAAM,EAC5B,OAAO,CAAC,EAAE,MAAM,EACA,MAAM,GAAE,MAAY,EACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAAA,EACjC,IAAI,CAAC,EAAE,MAAM,YAAA,EAQ9B;IAED,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,QAAQ,CAEvC;IAED,MAAM,IAAI,aAAa,CAStB;CACF;AAED,qCAAqC;AACrC,wBAAgB,QAAQ,CAAC,OAAO,SAAc,GAAG,KAAK,CAErD;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAEpF;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,OAAO,SAAiB,GAAG,KAAK,CAE5D;AAED,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,OAAO,SAAc,GAAG,KAAK,CAEtD;AAED,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,OAAO,SAAa,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAEvF;AAED,wCAAwC;AACxC,wBAAgB,WAAW,CAAC,OAAO,SAAsB,GAAG,KAAK,CAEhE;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;CAUE,CAAC;AAEnC,mFAAmF;AACnF,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,mBAAmB,CAAC;AAE/D,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,eAAe,CAEvE;AAED,+FAA+F;AAC/F,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,KAAK,CAOP"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/contract/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE;QACL,2CAA2C;QAC3C,IAAI,EAAE,MAAM,CAAC;QACb,8BAA8B;QAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,gCAAgC;QAChC,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,0BAA0B;QAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAcD;;;;GAIG;AACH,qBAAa,QAAQ,CACnB,KAAK,SAAS,MAAM,GAAG,MAAM,EAC7B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAC1F,SAAQ,KAAK;aAEK,IAAI,EAAE,KAAK;aAEX,MAAM,EAAE,MAAM;aACd,OAAO,CAAC,EAAE,QAAQ;aAClB,IAAI,CAAC,EAAE,MAAM;IAL/B,YACkB,IAAI,EAAE,KAAK,EAC3B,OAAO,CAAC,EAAE,MAAM,EACA,MAAM,GAAE,MAAY,EACpB,OAAO,CAAC,EAAE,QAAQ,YAAA,EAClB,IAAI,CAAC,EAAE,MAAM,YAAA,EAQ9B;IAED,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,QAAQ,CAEvC;IAED,MAAM,IAAI,aAAa,CAStB;CACF;AAED,qCAAqC;AACrC,wBAAgB,QAAQ,CAAC,OAAO,SAAc,GAAG,KAAK,CAErD;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAEpF;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,OAAO,SAAiB,GAAG,KAAK,CAE5D;AAED,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,OAAO,SAAc,GAAG,KAAK,CAEtD;AAED,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,OAAO,SAAa,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAEvF;AAED,wCAAwC;AACxC,wBAAgB,WAAW,CAAC,OAAO,SAAsB,GAAG,KAAK,CAEhE;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;CAUE,CAAC;AAEnC,mFAAmF;AACnF,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,mBAAmB,CAAC;AAE/D,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,eAAe,CAEvE;AAED,+FAA+F;AAC/F,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,KAAK,CAOP"}
@@ -20,13 +20,37 @@
20
20
  * The returned contracts are ordinary `ContractDef`s — `implement`, `createClient`
21
21
  * and the tool mounts treat them exactly as before.
22
22
  */
23
- import type { ContractDef, EndpointDef } from './define';
23
+ import type { ContractDef, ContractMeta, EndpointDef, Transport } from './define';
24
+ export type ContractFactoryToolExposure = 'explicit';
25
+ export interface ContractFactoryConfig {
26
+ /** Missing endpoint `expose` becomes HTTP-only; every tool surface is opt-in. */
27
+ toolExposure: ContractFactoryToolExposure;
28
+ }
29
+ export type ExplicitToolExposureEndpoints<T extends Record<string, EndpointDef>> = {
30
+ [K in keyof T]: T[K] extends {
31
+ expose: readonly Transport[];
32
+ } ? T[K] : T[K] & {
33
+ expose: readonly ['HTTP'];
34
+ };
35
+ };
36
+ /** A factory-defined contract whose concrete scope remains required in metadata. */
37
+ export interface ScopedContractDef<T extends Record<string, EndpointDef> = Record<string, EndpointDef>, TScope extends string = string> extends ContractDef<T, TScope> {
38
+ meta: ContractMeta<TScope> & {
39
+ scope: TScope;
40
+ };
41
+ }
24
42
  /** A `defineContract` whose `scope` is required and typed to `TScope`. */
25
43
  export type ScopedDefineContract<TScope extends string> = <const TContractScope extends TScope, const T extends Record<string, EndpointDef>>(meta: {
26
44
  prefix: string;
27
45
  scope: TContractScope;
28
46
  meta?: Record<string, unknown>;
29
- }, endpoints: T) => ContractDef<T, TContractScope>;
47
+ }, endpoints: T) => ScopedContractDef<T, TContractScope>;
48
+ /** Scoped contract authoring where every omitted exposure becomes HTTP-only. */
49
+ export type ExplicitScopedDefineContract<TScope extends string> = <const TContractScope extends TScope, const T extends Record<string, EndpointDef>>(meta: {
50
+ prefix: string;
51
+ scope: TContractScope;
52
+ meta?: Record<string, unknown>;
53
+ }, endpoints: T) => ScopedContractDef<ExplicitToolExposureEndpoints<T>, TContractScope>;
30
54
  /**
31
55
  * Build a `defineContract` that requires a `scope` from the application's own
32
56
  * union. Call once per app and re-export the returned `defineContract`.
@@ -34,4 +58,7 @@ export type ScopedDefineContract<TScope extends string> = <const TContractScope
34
58
  export declare function createContractFactory<TScope extends string>(): {
35
59
  defineContract: ScopedDefineContract<TScope>;
36
60
  };
61
+ export declare function createContractFactory<TScope extends string>(config: ContractFactoryConfig): {
62
+ defineContract: ExplicitScopedDefineContract<TScope>;
63
+ };
37
64
  //# sourceMappingURL=factory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/contract/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAGzD,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,CAAC,MAAM,SAAS,MAAM,IAAI,CACxD,KAAK,CAAC,cAAc,SAAS,MAAM,EACnC,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAE3C,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,cAAc,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EAC/E,SAAS,EAAE,CAAC,KACT,WAAW,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;AAEpC;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,SAAS,MAAM,KAAK;IAC9D,cAAc,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAC;CAC9C,CAiBA"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/contract/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGlF,MAAM,MAAM,2BAA2B,GAAG,UAAU,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,YAAY,EAAE,2BAA2B,CAAC;CAC3C;AAED,MAAM,MAAM,6BAA6B,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,IAAI;KAChF,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;QAAE,MAAM,EAAE,SAAS,SAAS,EAAE,CAAA;KAAE,GACzD,CAAC,CAAC,CAAC,CAAC,GACJ,CAAC,CAAC,CAAC,CAAC,GAAG;QAAE,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;KAAE;CACzC,CAAC;AAEF,oFAAoF;AACpF,MAAM,WAAW,iBAAiB,CAChC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACnE,MAAM,SAAS,MAAM,GAAG,MAAM,CAC9B,SAAQ,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC;IAC9B,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD;AAED,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,CAAC,MAAM,SAAS,MAAM,IAAI,CACxD,KAAK,CAAC,cAAc,SAAS,MAAM,EACnC,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAE3C,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,cAAc,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EAC/E,SAAS,EAAE,CAAC,KACT,iBAAiB,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;AAE1C,gFAAgF;AAChF,MAAM,MAAM,4BAA4B,CAAC,MAAM,SAAS,MAAM,IAAI,CAChE,KAAK,CAAC,cAAc,SAAS,MAAM,EACnC,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAE3C,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,cAAc,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EAC/E,SAAS,EAAE,CAAC,KACT,iBAAiB,CAAC,6BAA6B,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,SAAS,MAAM,KAAK;IAC9D,cAAc,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAC;CAC9C,CAAC;AACF,wBAAgB,qBAAqB,CAAC,MAAM,SAAS,MAAM,EACzD,MAAM,EAAE,qBAAqB,GAC5B;IACD,cAAc,EAAE,4BAA4B,CAAC,MAAM,CAAC,CAAC;CACtD,CAAC"}
@@ -1,6 +1,6 @@
1
1
  export { ALL_TRANSPORTS, type BodyHttpSuccessStatus, type ContractDef, type ContractMeta, defineContract, type EndpointDef, type EndpointFn, type EndpointResponseMeta, type EndpointToolAnnotations, type EndpointUiMeta, type FileDescriptor, type HandlerContext, type HeadEndpointDef, type HttpMethod, type HttpSuccessStatus, type MultipartFile, type ResponseMetadata, type RuntimeContext, type ScopedEndpointFn, type ScopedHttpClient, type ScopedUrlBuilder, type ScopedUrlFn, type Transport, type TransportSource, type TypedClient, type TypedHttpClient, type TypedUrlBuilder, } from './define';
2
2
  export { AppError, appError, badRequest, conflict, type ErrorEnvelope, forbidden, isStitchErrorCode, notFound, rateLimited, STITCH_ERROR_STATUS, type StitchErrorCode, unauthorized, } from './errors';
3
- export { type DefinedErrors, defineErrors, type ErrorThrower, } from './errors-factory';
4
- export { createContractFactory, type ScopedDefineContract } from './factory';
3
+ export { type DefinedAppError, type DefinedErrors, defineErrors, type ErrorDefinition, type ErrorDefinitions, type ErrorDetailsOutput, type ErrorDetailsSchema, type ErrorFactories, type ErrorFactory, type ErrorFactoryArguments, type FrozenErrorDefinitions, } from './errors-factory';
4
+ export { type ContractFactoryConfig, type ContractFactoryToolExposure, createContractFactory, type ExplicitScopedDefineContract, type ExplicitToolExposureEndpoints, type ScopedContractDef, type ScopedDefineContract, } from './factory';
5
5
  export { decodeCursor, encodeCursor, type Paginated, paginatedSchema, } from './pagination';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contract/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,KAAK,aAAa,EAClB,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,KAAK,eAAe,EACpB,YAAY,GACb,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,KAAK,aAAa,EAClB,YAAY,EACZ,KAAK,YAAY,GAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAE7E,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,KAAK,SAAS,EACd,eAAe,GAChB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contract/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,KAAK,aAAa,EAClB,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,KAAK,eAAe,EACpB,YAAY,GACb,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,EAChC,qBAAqB,EACrB,KAAK,4BAA4B,EACjC,KAAK,6BAA6B,EAClC,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,KAAK,SAAS,EACd,eAAe,GAChB,MAAM,cAAc,CAAC"}
@@ -16,8 +16,8 @@ import {
16
16
  paginatedSchema,
17
17
  rateLimited,
18
18
  unauthorized
19
- } from "../index-xax049k6.js";
20
- import"../index-809wc1tt.js";
19
+ } from "../index-r6czv7yt.js";
20
+ import"../index-s4qsmgwe.js";
21
21
  export {
22
22
  unauthorized,
23
23
  rateLimited,
@@ -6,7 +6,7 @@ import {
6
6
  isUnsafeKey,
7
7
  safeJsonParse,
8
8
  unauthorized
9
- } from "./index-sc67e454.js";
9
+ } from "./index-gex6gxhe.js";
10
10
 
11
11
  // src/server/middleware/cookies.ts
12
12
  function parseCookies(header) {
@@ -22,13 +22,15 @@ import {
22
22
  parseTrailingWildcard,
23
23
  recordedErrorMessage,
24
24
  resolveSocketIp,
25
+ resolveTraceContext,
25
26
  resolveTraceId,
27
+ runWithRequestContext,
26
28
  safeJsonParse,
27
29
  setRequestEndpoint,
28
30
  setRequestError,
29
31
  typedEntries,
30
32
  validateDeclaredOutput
31
- } from "./index-sc67e454.js";
33
+ } from "./index-gex6gxhe.js";
32
34
 
33
35
  // src/server/multipart.ts
34
36
  var DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
@@ -484,9 +486,6 @@ function timestamp() {
484
486
  });
485
487
  return `${t}.${now.getMilliseconds().toString().padStart(3, "0")}`;
486
488
  }
487
- function elapsedMs(startTime) {
488
- return performance.now() - startTime;
489
- }
490
489
  function safePath(pathname) {
491
490
  let out = "";
492
491
  for (let i = 0;i < pathname.length; i++) {
@@ -544,7 +543,7 @@ function shouldLog(pathname, method) {
544
543
  return !SKIP_PREFIXES.some((prefix) => pathname.startsWith(prefix));
545
544
  }
546
545
  function logIncoming(req, pathname, traceId, format, ipAddress) {
547
- const log = { traceId, startTime: performance.now() };
546
+ const log = { traceId };
548
547
  if (format === "pretty") {
549
548
  const mc = METHOD_COLOR[req.method] ?? c.dim;
550
549
  console.log(`${c.gray}[${timestamp()}]${c.reset} ${mc}${req.method}${c.reset} ${c.dim}${traceId}${c.reset} ${c.cyan}→${c.reset} ${safePath(pathname)} ${ipLabel(ipAddress ?? "")}`);
@@ -560,7 +559,7 @@ function structuredLine(own, extra) {
560
559
  }
561
560
  function logOutgoing(entry) {
562
561
  const { req, pathname, status, log, ipAddress, errorCode: errorCode2, durationMs, format, extra } = entry;
563
- const ms = elapsedMs(log.startTime);
562
+ const ms = durationMs;
564
563
  if (format === "json") {
565
564
  const own = {
566
565
  ts: new Date().toISOString(),
@@ -664,8 +663,9 @@ function applyResponseMetadata(target, metadata, endpointIdentity) {
664
663
  }
665
664
 
666
665
  // src/server/create.ts
666
+ var BODY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
667
667
  function createHandler(config) {
668
- const { cors, hooks, logging = false, trustProxy = false } = config;
668
+ const { cors, hooks, logging = false, observability, trustProxy = false } = config;
669
669
  if (cors)
670
670
  assertCorsConfig(cors);
671
671
  assertJsonBodyLimit(config.maxJsonBodyBytes, "HandlerConfig.maxJsonBodyBytes");
@@ -683,17 +683,17 @@ function createHandler(config) {
683
683
  };
684
684
  let traceResolverBroken = false;
685
685
  const warnedEnrichKeys = new Set;
686
- const resolveId = (req) => {
686
+ const resolveId = (req, fallback) => {
687
687
  if (!customTraceId)
688
- return resolveTraceId(req);
688
+ return fallback ?? resolveTraceId(req);
689
689
  try {
690
- return customTraceId(req) ?? resolveTraceId(req);
690
+ return customTraceId(req) ?? fallback ?? resolveTraceId(req);
691
691
  } catch (err) {
692
692
  if (!traceResolverBroken) {
693
693
  traceResolverBroken = true;
694
694
  warn("[stitchkit] `traceId` resolver threw — falling back to the framework resolver. " + "Ids will not match your observability context until it is fixed: " + `${err instanceof Error ? err.message : String(err)}`);
695
695
  }
696
- return resolveTraceId(req);
696
+ return fallback ?? resolveTraceId(req);
697
697
  }
698
698
  };
699
699
  const routeMap = buildRouteMap(normalizeGroups(config));
@@ -704,16 +704,19 @@ function createHandler(config) {
704
704
  const line = `[stitchkit] raw route ${shadow.rawRoute} shadows contract route ${shadow.pattern}` + ` → ${shadow.endpoint}${gate} will never run, and its hooks never apply`;
705
705
  warn(line);
706
706
  }
707
- async function dispatch(req, url, traceId, server, clientIp) {
707
+ async function dispatch(req, url, traceId, server, clientIp, startedAt) {
708
708
  const shouldLogRequest = logConfig !== null && shouldLog(url.pathname, req.method) && !shouldSkipLog(logConfig, req, url);
709
709
  const ipAddress = extractIp(req, clientIp) || undefined;
710
+ const payload = observability?.includePayload && BODY_METHODS.has(req.method) ? req.clone().json().catch(() => {
711
+ return;
712
+ }) : undefined;
710
713
  const logFormat = resolveLogFormat(logConfig?.format);
711
714
  let reqLog;
712
715
  if (shouldLogRequest && useDefaultLog) {
713
716
  reqLog = logIncoming(req, url.pathname, traceId, logFormat, ipAddress);
714
717
  }
715
718
  if (shouldLogRequest && customLogger) {
716
- reqLog = { traceId, startTime: performance.now() };
719
+ reqLog = { traceId };
717
720
  try {
718
721
  customLogger.debug(`${req.method} ${url.pathname}`, {
719
722
  traceId,
@@ -723,50 +726,58 @@ function createHandler(config) {
723
726
  });
724
727
  } catch {}
725
728
  }
726
- let logged = false;
727
- const logDone = (status, errorCode2) => {
728
- if (!reqLog || logged || !logConfig)
729
+ let completed = false;
730
+ const complete = (status, errorCode2) => {
731
+ if (completed)
729
732
  return;
730
- logged = true;
731
- try {
732
- const durationMs = Math.round(elapsedMs(reqLog.startTime));
733
- const collected = collectExtraLogFields(logConfig, req, url, {
734
- status,
735
- durationMs,
736
- errorCode: errorCode2
737
- });
738
- const frameworkFields = buildLogFields(req.method, url.pathname, status, durationMs, reqLog.traceId, errorCode2);
739
- const ownedFields = { ...frameworkFields, ip: ipAddress };
740
- for (const key of collected.enrichKeys) {
741
- const ownedByActiveSink = Object.hasOwn(ownedFields, key) || useDefaultLog && (key === "ts" || key === "level" || key === "msg");
742
- if (ownedByActiveSink && !warnedEnrichKeys.has(key)) {
743
- warnedEnrichKeys.add(key);
744
- warn(`[stitchkit] logging.enrich field "${key}" was discarded because the framework owns it`);
745
- }
746
- }
747
- const extra = collected.fields;
748
- if (useDefaultLog) {
749
- logOutgoing({
750
- req,
751
- pathname: url.pathname,
733
+ completed = true;
734
+ const durationMs = Math.round(Number(process.hrtime.bigint() - startedAt) / 1e6);
735
+ if (reqLog && logConfig) {
736
+ try {
737
+ const collected = collectExtraLogFields(logConfig, req, url, {
752
738
  status,
753
- log: reqLog,
754
- ipAddress,
755
- errorCode: errorCode2,
756
739
  durationMs,
757
- format: logFormat,
758
- extra
740
+ errorCode: errorCode2
759
741
  });
760
- }
761
- if (customLogger) {
762
- const level = levelForStatus(status);
763
- customLogger[level](`${req.method} ${url.pathname} ${status}${errorCode2 ? ` ${errorCode2}` : ""} ${durationMs}ms`, {
764
- ...extra,
765
- ...frameworkFields,
766
- ip: ipAddress
767
- });
768
- }
769
- } catch {}
742
+ const frameworkFields = buildLogFields(req.method, url.pathname, status, durationMs, reqLog.traceId, errorCode2);
743
+ const ownedFields = { ...frameworkFields, ip: ipAddress };
744
+ for (const key of collected.enrichKeys) {
745
+ const ownedByActiveSink = Object.hasOwn(ownedFields, key) || useDefaultLog && (key === "ts" || key === "level" || key === "msg");
746
+ if (ownedByActiveSink && !warnedEnrichKeys.has(key)) {
747
+ warnedEnrichKeys.add(key);
748
+ warn(`[stitchkit] logging.enrich field "${key}" was discarded because the framework owns it`);
749
+ }
750
+ }
751
+ const extra = collected.fields;
752
+ if (useDefaultLog) {
753
+ logOutgoing({
754
+ req,
755
+ pathname: url.pathname,
756
+ status,
757
+ log: reqLog,
758
+ ipAddress,
759
+ errorCode: errorCode2,
760
+ durationMs,
761
+ format: logFormat,
762
+ extra
763
+ });
764
+ }
765
+ if (customLogger) {
766
+ const level = levelForStatus(status);
767
+ customLogger[level](`${req.method} ${url.pathname} ${status}${errorCode2 ? ` ${errorCode2}` : ""} ${durationMs}ms`, {
768
+ ...extra,
769
+ ...frameworkFields,
770
+ ip: ipAddress
771
+ });
772
+ }
773
+ } catch {}
774
+ }
775
+ const context = getRequestContext();
776
+ if (observability && context) {
777
+ try {
778
+ observability.complete({ context, statusCode: status, durationMs, payload });
779
+ } catch {}
780
+ }
770
781
  };
771
782
  const respondError = async (err, errCtx, endpoint) => {
772
783
  const recordFailure = (normalized) => {
@@ -786,25 +797,27 @@ function createHandler(config) {
786
797
  if (response instanceof Response) {
787
798
  recordFailure();
788
799
  const withCors = applyCors(response, cors, req);
789
- logDone(withCors.status, errorCode(err));
800
+ complete(withCors.status, errorCode(err));
790
801
  return withCors;
791
802
  }
792
803
  } catch {}
793
804
  }
794
805
  const appErr = normalizeError(err);
795
806
  recordFailure(appErr);
796
- logDone(appErr.status, appErr.code);
807
+ complete(appErr.status, appErr.code);
797
808
  return json(appErr.toJSON(), appErr.status, cors, req);
798
809
  };
799
810
  if (cors && req.method === "OPTIONS") {
800
- return corsPreflightResponse(cors, req);
811
+ const response = corsPreflightResponse(cors, req);
812
+ complete(response.status);
813
+ return response;
801
814
  }
802
815
  try {
803
816
  if (hooks?.onRequest) {
804
817
  const earlyResponse = await hooks.onRequest(req);
805
818
  if (earlyResponse instanceof Response) {
806
819
  const withCors = applyCors(earlyResponse, cors, req);
807
- logDone(withCors.status);
820
+ complete(withCors.status);
808
821
  return withCors;
809
822
  }
810
823
  }
@@ -821,7 +834,7 @@ function createHandler(config) {
821
834
  ipAddress
822
835
  });
823
836
  const withCors = applyCors(res, cors, req);
824
- logDone(withCors.status);
837
+ complete(withCors.status);
825
838
  return withCors;
826
839
  } catch (err) {
827
840
  return respondError(err);
@@ -863,7 +876,7 @@ function createHandler(config) {
863
876
  headers: result.headers
864
877
  }) : result;
865
878
  const rawRes = applyCors(response, cors, req);
866
- logDone(rawRes.status);
879
+ complete(rawRes.status);
867
880
  return rawRes;
868
881
  }
869
882
  if (groupHooks?.afterHandle) {
@@ -894,11 +907,11 @@ function createHandler(config) {
894
907
  applyResponseMetadata(responseHeaders, responseMetadata, `${method.serviceName}.${method.key}`);
895
908
  if (!method.outputSchema) {
896
909
  const empty = new Response(null, { status: responseStatus, headers: responseHeaders });
897
- logDone(responseStatus);
910
+ complete(responseStatus);
898
911
  return empty;
899
912
  }
900
913
  const body = Response.json(result, { status: responseStatus, headers: responseHeaders });
901
- logDone(responseStatus);
914
+ complete(responseStatus);
902
915
  return body;
903
916
  } catch (err) {
904
917
  return respondError(err, ctx, method);
@@ -906,16 +919,37 @@ function createHandler(config) {
906
919
  }
907
920
  return async (req, server) => {
908
921
  const url = new URL(req.url, "http://localhost");
909
- const traceId = resolveId(req);
922
+ const requestStartedAt = process.hrtime.bigint();
910
923
  const clientIp = {
911
924
  trustProxy,
912
925
  socketIp: resolveSocketIp(req, server)
913
926
  };
914
- const response = await dispatch(req, url, traceId, server, clientIp);
915
- try {
916
- response.headers.set("x-request-id", traceId);
917
- } catch {}
918
- return response;
927
+ const run = async (traceId2, startedAt) => {
928
+ const response = await dispatch(req, url, traceId2, server, clientIp, startedAt);
929
+ try {
930
+ response.headers.set("x-request-id", traceId2);
931
+ } catch {}
932
+ return response;
933
+ };
934
+ const activeContext = getRequestContext();
935
+ if (activeContext) {
936
+ return run(activeContext.trace.traceId, activeContext.startedAt);
937
+ }
938
+ if (!observability)
939
+ return run(resolveId(req), requestStartedAt);
940
+ const trace = resolveTraceContext(req);
941
+ const traceId = resolveId(req, trace.traceId);
942
+ const ipAddress = extractIp(req, clientIp) || undefined;
943
+ const userAgent = req.headers.get("user-agent") ?? undefined;
944
+ return runWithRequestContext({
945
+ source: "http",
946
+ method: req.method,
947
+ path: url.pathname,
948
+ startedAt: requestStartedAt,
949
+ trace: { ...trace, traceId },
950
+ ...ipAddress !== undefined && { ipAddress },
951
+ ...userAgent !== undefined && { userAgent }
952
+ }, () => run(traceId, requestStartedAt));
919
953
  };
920
954
  }
921
955
  function normalizeGroups(config) {