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.
- package/README.md +22 -5
- package/dist/cli.js +2 -2
- package/dist/contract/errors-factory.d.ts +88 -15
- package/dist/contract/errors-factory.d.ts.map +1 -1
- package/dist/contract/errors.d.ts +4 -4
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/factory.d.ts +29 -2
- package/dist/contract/factory.d.ts.map +1 -1
- package/dist/contract/index.d.ts +2 -2
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +2 -2
- package/dist/{index-dnkefke9.js → index-310bfer5.js} +1 -1
- package/dist/{index-4e1c0hsw.js → index-esqmem78.js} +101 -67
- package/dist/{index-sc67e454.js → index-gex6gxhe.js} +7 -4
- package/dist/{index-ncqqn1bc.js → index-h2wdcsby.js} +95 -17
- package/dist/{index-xax049k6.js → index-r6czv7yt.js} +58 -22
- package/dist/{index-809wc1tt.js → index-s4qsmgwe.js} +4 -1
- package/dist/index.js +2 -2
- package/dist/internal/typed.d.ts +8 -0
- package/dist/internal/typed.d.ts.map +1 -1
- package/dist/node.js +2 -2
- package/dist/observability/audit.d.ts +27 -15
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/event.d.ts +2 -2
- package/dist/observability/index.d.ts +4 -4
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +70 -59
- package/dist/react.js +1 -1
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/error-hook.d.ts +8 -4
- package/dist/server/error-hook.d.ts.map +1 -1
- package/dist/server/index.js +7 -6
- package/dist/server/logger.d.ts +0 -1
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/types.d.ts +8 -13
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts +1 -1
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/flatten-join.d.ts.map +1 -1
- package/dist/tools/list-names.d.ts +8 -6
- package/dist/tools/list-names.d.ts.map +1 -1
- package/dist/tools/manifest.d.ts +13 -5
- package/dist/tools/manifest.d.ts.map +1 -1
- package/dist/tools/mcp-handler.d.ts +5 -4
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.d.ts +3 -2
- package/dist/tools/mcp-stdio.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +49 -9
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/native-mcp.d.ts +4 -18
- package/dist/tools/native-mcp.d.ts.map +1 -1
- package/dist/tools/runtime-tool.d.ts +32 -1
- package/dist/tools/runtime-tool.d.ts.map +1 -1
- package/dist/tools/surface.d.ts +37 -0
- package/dist/tools/surface.d.ts.map +1 -0
- package/dist/tools/toolkit.d.ts +4 -4
- package/dist/tools/toolkit.d.ts.map +1 -1
- package/dist/tools/transports.d.ts +11 -9
- package/dist/tools/transports.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts +3 -3
- package/dist/tools.d.ts +6 -6
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +330 -155
- package/llms-full.txt +524 -131
- package/llms.txt +1 -0
- 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
|
-
##
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
Socket.IO
|
|
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-
|
|
7
|
+
} from "./index-h2wdcsby.js";
|
|
8
8
|
import"./index-frfyw9fa.js";
|
|
9
9
|
import"./index-x3fcszf8.js";
|
|
10
|
-
import"./index-
|
|
10
|
+
import"./index-gex6gxhe.js";
|
|
11
11
|
export {
|
|
12
12
|
pollUntilDone,
|
|
13
13
|
parseCliArgs,
|
|
@@ -1,18 +1,91 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
17
|
-
export declare function defineErrors<const
|
|
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":"
|
|
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:
|
|
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?:
|
|
27
|
+
readonly details?: TDetails | undefined;
|
|
28
28
|
readonly hint?: string | undefined;
|
|
29
|
-
constructor(code:
|
|
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,
|
|
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) =>
|
|
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;
|
|
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"}
|
package/dist/contract/index.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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"}
|
package/dist/contract/index.js
CHANGED
|
@@ -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-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
727
|
-
const
|
|
728
|
-
if (
|
|
729
|
+
let completed = false;
|
|
730
|
+
const complete = (status, errorCode2) => {
|
|
731
|
+
if (completed)
|
|
729
732
|
return;
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
758
|
-
extra
|
|
740
|
+
errorCode: errorCode2
|
|
759
741
|
});
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
910
|
+
complete(responseStatus);
|
|
898
911
|
return empty;
|
|
899
912
|
}
|
|
900
913
|
const body = Response.json(result, { status: responseStatus, headers: responseHeaders });
|
|
901
|
-
|
|
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
|
|
922
|
+
const requestStartedAt = process.hrtime.bigint();
|
|
910
923
|
const clientIp = {
|
|
911
924
|
trustProxy,
|
|
912
925
|
socketIp: resolveSocketIp(req, server)
|
|
913
926
|
};
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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) {
|