@schmock/core 2.3.0 → 2.4.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 (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/abort.d.ts +3 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +30 -0
  6. package/dist/builder.d.ts +31 -10
  7. package/dist/builder.d.ts.map +1 -1
  8. package/dist/builder.js +760 -190
  9. package/dist/constants.d.ts +38 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/constants.js +123 -2
  12. package/dist/errors.d.ts +14 -3
  13. package/dist/errors.d.ts.map +1 -1
  14. package/dist/errors.js +41 -6
  15. package/dist/helpers.d.ts +9 -0
  16. package/dist/helpers.d.ts.map +1 -1
  17. package/dist/helpers.js +18 -2
  18. package/dist/http-helpers.d.ts +39 -4
  19. package/dist/http-helpers.d.ts.map +1 -1
  20. package/dist/http-helpers.js +147 -49
  21. package/dist/index.d.ts +292 -34
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +8 -3
  24. package/dist/interceptor.d.ts +11 -1
  25. package/dist/interceptor.d.ts.map +1 -1
  26. package/dist/interceptor.js +316 -169
  27. package/dist/parser.d.ts.map +1 -1
  28. package/dist/parser.js +12 -3
  29. package/dist/plugin-pipeline.d.ts +3 -3
  30. package/dist/plugin-pipeline.d.ts.map +1 -1
  31. package/dist/plugin-pipeline.js +29 -14
  32. package/dist/response-normalizer.d.ts +16 -0
  33. package/dist/response-normalizer.d.ts.map +1 -0
  34. package/dist/response-normalizer.js +316 -0
  35. package/dist/response-parser.d.ts.map +1 -1
  36. package/dist/response-parser.js +39 -2
  37. package/dist/route-matcher.d.ts +3 -0
  38. package/dist/route-matcher.d.ts.map +1 -1
  39. package/dist/route-matcher.js +12 -6
  40. package/dist/types.d.ts +7 -2
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +18 -6
  43. package/src/audit-core-builder.test.ts +0 -218
  44. package/src/audit-response-guard.test.ts +0 -38
  45. package/src/binary.test.ts +0 -126
  46. package/src/binary.ts +0 -11
  47. package/src/builder.test.ts +0 -289
  48. package/src/builder.ts +0 -748
  49. package/src/constants.test.ts +0 -99
  50. package/src/constants.ts +0 -73
  51. package/src/debug.test.ts +0 -241
  52. package/src/delay.test.ts +0 -319
  53. package/src/dist-shape.test.ts +0 -35
  54. package/src/errors.test.ts +0 -223
  55. package/src/errors.ts +0 -130
  56. package/src/factory.test.ts +0 -133
  57. package/src/helpers.test.ts +0 -147
  58. package/src/helpers.ts +0 -58
  59. package/src/http-helpers.test.ts +0 -39
  60. package/src/http-helpers.ts +0 -149
  61. package/src/index.ts +0 -152
  62. package/src/interceptor.test.ts +0 -488
  63. package/src/interceptor.ts +0 -407
  64. package/src/namespace.test.ts +0 -274
  65. package/src/parser.property.test.ts +0 -630
  66. package/src/parser.test.ts +0 -148
  67. package/src/parser.ts +0 -64
  68. package/src/plugin-pipeline.ts +0 -208
  69. package/src/plugin-system.test.ts +0 -697
  70. package/src/response-parser.ts +0 -114
  71. package/src/response-parsing.test.ts +0 -333
  72. package/src/route-matcher.ts +0 -69
  73. package/src/route-matching.test.ts +0 -394
  74. package/src/server.test.ts +0 -234
  75. package/src/smart-defaults.test.ts +0 -361
  76. package/src/steps/async-support.steps.ts +0 -421
  77. package/src/steps/audit-core-builder.steps.ts +0 -188
  78. package/src/steps/audit-onerror-tuple.steps.ts +0 -83
  79. package/src/steps/basic-usage.steps.ts +0 -245
  80. package/src/steps/developer-experience.steps.ts +0 -204
  81. package/src/steps/error-handling.steps.ts +0 -555
  82. package/src/steps/fetch-interceptor.steps.ts +0 -457
  83. package/src/steps/fluent-api.steps.ts +0 -255
  84. package/src/steps/http-methods.steps.ts +0 -342
  85. package/src/steps/lifecycle-events.steps.ts +0 -149
  86. package/src/steps/performance-reliability.steps.ts +0 -90
  87. package/src/steps/plugin-integration.steps.ts +0 -334
  88. package/src/steps/request-history.steps.ts +0 -458
  89. package/src/steps/response-delay.steps.ts +0 -88
  90. package/src/steps/route-key-format.steps.ts +0 -99
  91. package/src/steps/standalone-server.steps.ts +0 -227
  92. package/src/steps/state-concurrency.steps.ts +0 -167
  93. package/src/types.ts +0 -36
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-pipeline.d.ts","sourceRoot":"","sources":["../src/plugin-pipeline.ts"],"names":[],"mappings":"AAEA,uEAAuE;AACvE,UAAU,cAAc;IACtB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9D;AAED,UAAU,cAAc;IACtB,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAqDD,kEAAkE;AAClE,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,EACzB,OAAO,EAAE,OAAO,CAAC,aAAa,EAC9B,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,cAAc,CAAC,CAiDzB;AAED,gFAAgF;AAChF,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,EACzB,OAAO,EAAE,OAAO,CAAC,aAAa,EAC9B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,cAAc,CAAC,CAUzB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,EACzB,OAAO,EAAE,OAAO,CAAC,aAAa,EAC9B,eAAe,EAAE,OAAO,EACxB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,cAAc,CAAC,CA0DzB"}
1
+ {"version":3,"file":"plugin-pipeline.d.ts","sourceRoot":"","sources":["../src/plugin-pipeline.ts"],"names":[],"mappings":"AAGA,uEAAuE;AACvE,UAAU,cAAc;IACtB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9D;AAED,UAAU,cAAc;IACtB,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAmED,kEAAkE;AAClE,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,SAAS,OAAO,CAAC,MAAM,EAAE,EAClC,OAAO,EAAE,OAAO,CAAC,aAAa,EAC9B,MAAM,EAAE,cAAc,EACtB,MAAM,0BAAiB,GACtB,OAAO,CAAC,cAAc,CAAC,CAwDzB;AAED,gFAAgF;AAChF,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,SAAS,OAAO,CAAC,MAAM,EAAE,EAClC,OAAO,EAAE,OAAO,CAAC,aAAa,EAC9B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,cAAc,EACtB,MAAM,0BAAiB,GACtB,OAAO,CAAC,cAAc,CAAC,CAmBzB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,SAAS,OAAO,CAAC,MAAM,EAAE,EAClC,OAAO,EAAE,OAAO,CAAC,aAAa,EAC9B,eAAe,EAAE,OAAO,EACxB,MAAM,EAAE,cAAc,EACtB,MAAM,0BAAiB,GACtB,OAAO,CAAC,cAAc,CAAC,CAiEzB"}
@@ -1,3 +1,4 @@
1
+ import { awaitWithAbort, throwIfAborted } from "./abort.js";
1
2
  import { errorMessage, PluginError } from "./errors.js";
2
3
  function isPluginResult(value) {
3
4
  return (typeof value === "object" &&
@@ -6,14 +7,19 @@ function isPluginResult(value) {
6
7
  typeof value.context === "object" &&
7
8
  value.context !== null);
8
9
  }
9
- async function recoverPluginError(plugins, startIndex, error, context, logger) {
10
+ function preserveRequestSignal(context, signal) {
11
+ return context.signal === signal ? context : { ...context, signal };
12
+ }
13
+ async function recoverPluginError(plugins, startIndex, error, context, logger, signal) {
10
14
  let currentError = error instanceof Error ? error : new Error(errorMessage(error));
11
15
  for (let index = startIndex; index < plugins.length; index += 1) {
16
+ throwIfAborted(signal);
12
17
  const plugin = plugins[index];
13
18
  if (!plugin.onError)
14
19
  continue;
15
20
  try {
16
- const errorResult = await plugin.onError(currentError, context);
21
+ const errorResult = await awaitWithAbort(plugin.onError(currentError, context), signal);
22
+ throwIfAborted(signal);
17
23
  if (errorResult instanceof Error) {
18
24
  currentError = errorResult;
19
25
  continue;
@@ -24,6 +30,7 @@ async function recoverPluginError(plugins, startIndex, error, context, logger) {
24
30
  }
25
31
  }
26
32
  catch (hookError) {
33
+ throwIfAborted(signal);
27
34
  currentError =
28
35
  hookError instanceof Error
29
36
  ? hookError
@@ -34,21 +41,23 @@ async function recoverPluginError(plugins, startIndex, error, context, logger) {
34
41
  return { handled: false, error: currentError };
35
42
  }
36
43
  /** Run request guards before route code is allowed to execute. */
37
- export async function runPluginBeforeRequest(plugins, context, logger) {
38
- let currentContext = context;
44
+ export async function runPluginBeforeRequest(plugins, context, logger, signal = context.signal) {
45
+ let currentContext = preserveRequestSignal(context, signal);
39
46
  for (let index = 0; index < plugins.length; index += 1) {
47
+ throwIfAborted(signal);
40
48
  const plugin = plugins[index];
41
49
  if (!plugin.beforeRequest)
42
50
  continue;
43
51
  logger.log("pipeline", `Running beforeRequest: ${plugin.name}`);
44
52
  try {
45
- const result = await plugin.beforeRequest(currentContext);
53
+ const result = await awaitWithAbort(plugin.beforeRequest(currentContext), signal);
54
+ throwIfAborted(signal);
46
55
  if (result === undefined)
47
56
  continue;
48
57
  if (!isPluginResult(result)) {
49
58
  throw new Error(`Plugin ${plugin.name} didn't return valid result`);
50
59
  }
51
- currentContext = result.context;
60
+ currentContext = preserveRequestSignal(result.context, signal);
52
61
  if (result.response !== undefined) {
53
62
  logger.log("pipeline", `Plugin ${plugin.name} rejected request`);
54
63
  return {
@@ -59,8 +68,9 @@ export async function runPluginBeforeRequest(plugins, context, logger) {
59
68
  }
60
69
  }
61
70
  catch (error) {
71
+ throwIfAborted(signal);
62
72
  logger.log("pipeline", `Plugin ${plugin.name} beforeRequest failed: ${errorMessage(error)}`);
63
- const recovery = await recoverPluginError(plugins, index, error, currentContext, logger);
73
+ const recovery = await recoverPluginError(plugins, index, error, currentContext, logger, signal);
64
74
  if (recovery.handled) {
65
75
  return {
66
76
  context: currentContext,
@@ -75,8 +85,10 @@ export async function runPluginBeforeRequest(plugins, context, logger) {
75
85
  return { context: currentContext };
76
86
  }
77
87
  /** Give pipeline error handlers a chance to recover a route-generator error. */
78
- export async function recoverGeneratorError(plugins, context, error, logger) {
79
- const recovery = await recoverPluginError(plugins, 0, error, context, logger);
88
+ export async function recoverGeneratorError(plugins, context, error, logger, signal = context.signal) {
89
+ throwIfAborted(signal);
90
+ const recovery = await recoverPluginError(plugins, 0, error, context, logger, signal);
91
+ throwIfAborted(signal);
80
92
  if (recovery.handled) {
81
93
  return {
82
94
  context,
@@ -91,19 +103,21 @@ export async function recoverGeneratorError(plugins, context, error, logger) {
91
103
  * First plugin to set response becomes generator, subsequent plugins transform
92
104
  * Handles plugin errors via onError hooks
93
105
  */
94
- export async function runPluginPipeline(plugins, context, initialResponse, logger) {
95
- let currentContext = context;
106
+ export async function runPluginPipeline(plugins, context, initialResponse, logger, signal = context.signal) {
107
+ let currentContext = preserveRequestSignal(context, signal);
96
108
  let response = initialResponse;
97
109
  logger.log("pipeline", `Running plugin pipeline for ${plugins.length} plugins`);
98
110
  for (let index = 0; index < plugins.length; index += 1) {
111
+ throwIfAborted(signal);
99
112
  const plugin = plugins[index];
100
113
  logger.log("pipeline", `Processing plugin: ${plugin.name}`);
101
114
  try {
102
- const result = await plugin.process(currentContext, response);
115
+ const result = await awaitWithAbort(plugin.process(currentContext, response), signal);
116
+ throwIfAborted(signal);
103
117
  if (!isPluginResult(result)) {
104
118
  throw new Error(`Plugin ${plugin.name} didn't return valid result`);
105
119
  }
106
- currentContext = result.context;
120
+ currentContext = preserveRequestSignal(result.context, signal);
107
121
  // First plugin to set response becomes the generator
108
122
  if (result.response !== undefined &&
109
123
  (response === undefined || response === null)) {
@@ -116,8 +130,9 @@ export async function runPluginPipeline(plugins, context, initialResponse, logge
116
130
  }
117
131
  }
118
132
  catch (error) {
133
+ throwIfAborted(signal);
119
134
  logger.log("pipeline", `Plugin ${plugin.name} failed: ${errorMessage(error)}`);
120
- const recovery = await recoverPluginError(plugins, index, error, currentContext, logger);
135
+ const recovery = await recoverPluginError(plugins, index, error, currentContext, logger, signal);
121
136
  if (recovery.handled) {
122
137
  return {
123
138
  context: currentContext,
@@ -0,0 +1,16 @@
1
+ type OwnedBytes = ReturnType<typeof Uint8Array.of>;
2
+ interface NormalizableResponse {
3
+ status: number;
4
+ body: unknown;
5
+ headers?: Record<string, string>;
6
+ }
7
+ /**
8
+ * Validate and stabilize a response before it reaches a transport adapter.
9
+ */
10
+ export declare function normalizeResponse(response: NormalizableResponse, method: string): Schmock.Response;
11
+ /**
12
+ * Encode a response body using its normalized content type semantics.
13
+ */
14
+ export declare function serializeResponseBody(response: Schmock.Response): OwnedBytes | undefined;
15
+ export {};
16
+ //# sourceMappingURL=response-normalizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-normalizer.d.ts","sourceRoot":"","sources":["../src/response-normalizer.ts"],"names":[],"mappings":"AAwBA,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,EAAE,CAAC,CAAC;AAEnD,UAAU,oBAAoB;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAmSD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,oBAAoB,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,QAAQ,CAmClB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,OAAO,CAAC,QAAQ,GACzB,UAAU,GAAG,SAAS,CAcxB"}
@@ -0,0 +1,316 @@
1
+ import { isBinaryBody } from "./binary.js";
2
+ import { errorMessage, InvalidResponseError } from "./errors.js";
3
+ const BODY_FORBIDDEN_STATUSES = new Set([204, 205, 304]);
4
+ const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
5
+ const FRAMING_HEADERS = new Set([
6
+ "content-length",
7
+ "trailer",
8
+ "transfer-encoding",
9
+ ]);
10
+ /**
11
+ * Headers that describe the connection rather than the message (RFC 9110 §7.6.1).
12
+ * Kept separate from {@link FRAMING_HEADERS}, which is parameterised by
13
+ * `preserveContentLength`: these are never a route's to send, so the strip is
14
+ * unconditional.
15
+ */
16
+ const HOP_BY_HOP_HEADERS = new Set([
17
+ "connection",
18
+ "keep-alive",
19
+ "proxy-authenticate",
20
+ "proxy-authorization",
21
+ "te",
22
+ "upgrade",
23
+ ]);
24
+ function hasInvalidHeaderValue(value) {
25
+ for (let index = 0; index < value.length; index += 1) {
26
+ const code = value.charCodeAt(index);
27
+ if (code <= 8 || (code >= 10 && code <= 31) || code === 127)
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ function validateStatus(status) {
33
+ if (typeof status !== "number" ||
34
+ !Number.isFinite(status) ||
35
+ !Number.isInteger(status) ||
36
+ status < 200 ||
37
+ status > 599) {
38
+ throw new InvalidResponseError("status must be a finite integer from 200 through 599", { status });
39
+ }
40
+ }
41
+ function headerEntries(headers) {
42
+ if (typeof headers !== "object" ||
43
+ headers === null ||
44
+ Array.isArray(headers)) {
45
+ throw new InvalidResponseError("headers must be a string record");
46
+ }
47
+ try {
48
+ const entries = [];
49
+ for (const name of Reflect.ownKeys(headers)) {
50
+ const descriptor = Object.getOwnPropertyDescriptor(headers, name);
51
+ if (!descriptor?.enumerable)
52
+ continue;
53
+ if (typeof name !== "string") {
54
+ throw new InvalidResponseError("header names must be strings");
55
+ }
56
+ const value = headers[name];
57
+ if (typeof value !== "string") {
58
+ throw new InvalidResponseError("header values must be strings", {
59
+ headerName: name,
60
+ });
61
+ }
62
+ entries.push([name, value]);
63
+ }
64
+ return entries;
65
+ }
66
+ catch (error) {
67
+ if (error instanceof InvalidResponseError)
68
+ throw error;
69
+ throw new InvalidResponseError("headers could not be read", {
70
+ cause: errorMessage(error),
71
+ });
72
+ }
73
+ }
74
+ function normalizeHeadersWithPlatform(entries, PlatformHeaders) {
75
+ try {
76
+ const normalized = {};
77
+ for (const [name, value] of entries) {
78
+ try {
79
+ const headers = new PlatformHeaders([[name, value]]);
80
+ normalized[name] = headers.get(name) ?? value;
81
+ }
82
+ catch (error) {
83
+ throw new InvalidResponseError("header is invalid", {
84
+ cause: errorMessage(error),
85
+ headerName: name,
86
+ });
87
+ }
88
+ }
89
+ return normalized;
90
+ }
91
+ catch (error) {
92
+ if (error instanceof InvalidResponseError)
93
+ throw error;
94
+ throw new InvalidResponseError("headers could not be normalized", {
95
+ cause: errorMessage(error),
96
+ });
97
+ }
98
+ }
99
+ function normalizeHeadersWithoutPlatform(entries) {
100
+ const normalized = {};
101
+ for (const [name, value] of entries) {
102
+ if (!HEADER_NAME_PATTERN.test(name) || hasInvalidHeaderValue(value)) {
103
+ throw new InvalidResponseError("header is invalid", {
104
+ headerName: name,
105
+ });
106
+ }
107
+ normalized[name] = value.replace(/^[\t ]+|[\t ]+$/g, "");
108
+ }
109
+ return normalized;
110
+ }
111
+ function normalizeHeaders(headers) {
112
+ const entries = headerEntries(headers);
113
+ const seenNames = new Set();
114
+ for (const [name, value] of entries) {
115
+ const normalizedName = name.toLowerCase();
116
+ if (seenNames.has(normalizedName)) {
117
+ throw new InvalidResponseError("header names must be unique", {
118
+ headerName: name,
119
+ });
120
+ }
121
+ if (!HEADER_NAME_PATTERN.test(name) || hasInvalidHeaderValue(value)) {
122
+ throw new InvalidResponseError("header is invalid", {
123
+ headerName: name,
124
+ });
125
+ }
126
+ seenNames.add(normalizedName);
127
+ }
128
+ const PlatformHeaders = globalThis.Headers;
129
+ return typeof PlatformHeaders === "function"
130
+ ? normalizeHeadersWithPlatform(entries, PlatformHeaders)
131
+ : normalizeHeadersWithoutPlatform(entries);
132
+ }
133
+ function copyBinaryBody(body) {
134
+ try {
135
+ if (body instanceof Uint8Array) {
136
+ // Uint8Array.prototype.slice species-creates, so subclasses such as
137
+ // Node's Buffer keep their type while the bytes are still copied.
138
+ // body.slice() would not: Buffer overrides slice() to return a view.
139
+ return Uint8Array.prototype.slice.call(body);
140
+ }
141
+ const source = body instanceof ArrayBuffer
142
+ ? new Uint8Array(body)
143
+ : new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
144
+ return Uint8Array.from(source);
145
+ }
146
+ catch (error) {
147
+ throw new InvalidResponseError("binary body could not be copied", {
148
+ cause: errorMessage(error),
149
+ });
150
+ }
151
+ }
152
+ function bodyType(value) {
153
+ if (value === null)
154
+ return "null";
155
+ if (typeof value !== "object")
156
+ return typeof value;
157
+ try {
158
+ return Object.prototype.toString.call(value);
159
+ }
160
+ catch {
161
+ return "object";
162
+ }
163
+ }
164
+ function hasFunctionProperty(value, property) {
165
+ return typeof Reflect.get(value, property) === "function";
166
+ }
167
+ function assertJsonCompatible(value) {
168
+ if (value === null)
169
+ return;
170
+ switch (typeof value) {
171
+ case "boolean":
172
+ case "string":
173
+ return;
174
+ case "number":
175
+ if (Number.isFinite(value))
176
+ return;
177
+ throw new InvalidResponseError("body contains a non-finite number", {
178
+ bodyType: "number",
179
+ });
180
+ case "undefined":
181
+ case "function":
182
+ case "symbol":
183
+ case "bigint":
184
+ throw new InvalidResponseError(`body contains an unsupported ${typeof value} value`, { bodyType: typeof value });
185
+ case "object":
186
+ break;
187
+ }
188
+ try {
189
+ if (isBinaryBody(value)) {
190
+ throw new InvalidResponseError("binary values are supported only as the top-level body", { bodyType: bodyType(value) });
191
+ }
192
+ if (hasFunctionProperty(value, "then") ||
193
+ hasFunctionProperty(value, "getReader") ||
194
+ hasFunctionProperty(value, "getWriter") ||
195
+ (hasFunctionProperty(value, "pipe") &&
196
+ hasFunctionProperty(value, "on")) ||
197
+ hasFunctionProperty(value, Symbol.asyncIterator)) {
198
+ throw new InvalidResponseError("promise, iterable, and stream bodies are unsupported", { bodyType: bodyType(value) });
199
+ }
200
+ for (const key of Reflect.ownKeys(value)) {
201
+ if (typeof key === "symbol" &&
202
+ Object.getOwnPropertyDescriptor(value, key)?.enumerable) {
203
+ throw new InvalidResponseError("body contains an enumerable symbol property", { bodyType: bodyType(value) });
204
+ }
205
+ }
206
+ if (!Array.isArray(value) &&
207
+ Object.prototype.toString.call(value) !== "[object Object]") {
208
+ throw new InvalidResponseError("body contains an unsupported object", {
209
+ bodyType: bodyType(value),
210
+ });
211
+ }
212
+ }
213
+ catch (error) {
214
+ if (error instanceof InvalidResponseError)
215
+ throw error;
216
+ throw new InvalidResponseError("body could not be inspected", {
217
+ bodyType: bodyType(value),
218
+ cause: errorMessage(error),
219
+ });
220
+ }
221
+ }
222
+ function stringifyJsonBody(body) {
223
+ try {
224
+ const serialized = JSON.stringify(body, (_key, value) => {
225
+ assertJsonCompatible(value);
226
+ return value;
227
+ });
228
+ if (serialized !== undefined)
229
+ return serialized;
230
+ throw new InvalidResponseError("body did not produce JSON", {
231
+ bodyType: bodyType(body),
232
+ });
233
+ }
234
+ catch (error) {
235
+ if (error instanceof InvalidResponseError)
236
+ throw error;
237
+ throw new InvalidResponseError("body is not JSON-serializable", {
238
+ bodyType: bodyType(body),
239
+ cause: errorMessage(error),
240
+ });
241
+ }
242
+ }
243
+ function normalizeBody(body) {
244
+ if (body === undefined || typeof body === "string")
245
+ return body;
246
+ if (isBinaryBody(body))
247
+ return copyBinaryBody(body);
248
+ return JSON.parse(stringifyJsonBody(body));
249
+ }
250
+ function removeFramingHeaders(headers, preserveContentLength = false) {
251
+ for (const name of Object.keys(headers)) {
252
+ const normalizedName = name.toLowerCase();
253
+ if (FRAMING_HEADERS.has(normalizedName) &&
254
+ !(preserveContentLength && normalizedName === "content-length")) {
255
+ delete headers[name];
256
+ }
257
+ }
258
+ }
259
+ function removeHopByHopHeaders(headers) {
260
+ for (const name of Object.keys(headers)) {
261
+ if (HOP_BY_HOP_HEADERS.has(name.toLowerCase()))
262
+ delete headers[name];
263
+ }
264
+ }
265
+ /**
266
+ * Validate and stabilize a response before it reaches a transport adapter.
267
+ */
268
+ export function normalizeResponse(response, method) {
269
+ const status = response.status;
270
+ validateStatus(status);
271
+ const headers = normalizeHeaders(response.headers ?? {});
272
+ const normalizedMethod = method.toUpperCase();
273
+ const body = normalizedMethod === "HEAD" || BODY_FORBIDDEN_STATUSES.has(status)
274
+ ? undefined
275
+ : normalizeBody(response.body);
276
+ // HEAD and 304 may keep an entity Content-Length (RFC 9110), but 204/205
277
+ // never carry one. Trailer and Transfer-Encoding are always removed —
278
+ // Node's writeHead rejects them on bodyless responses, killing the socket
279
+ // before any bytes reach the client.
280
+ if (status === 204 || status === 205) {
281
+ removeFramingHeaders(headers);
282
+ }
283
+ else {
284
+ removeFramingHeaders(headers, normalizedMethod === "HEAD" || status === 304);
285
+ }
286
+ // A hop-by-hop header belongs to the connection, which the transport owns:
287
+ // a route that emits `Connection: close` announces a close the server will
288
+ // not perform, and a conformant client then discards the next response on
289
+ // that socket. Adapters that genuinely mean it (an ingress rejection) add it
290
+ // after normalization, through their own extra-headers channel.
291
+ removeHopByHopHeaders(headers);
292
+ return {
293
+ status,
294
+ body,
295
+ headers,
296
+ };
297
+ }
298
+ /**
299
+ * Encode a response body using its normalized content type semantics.
300
+ */
301
+ export function serializeResponseBody(response) {
302
+ const status = response.status;
303
+ validateStatus(status);
304
+ if (BODY_FORBIDDEN_STATUSES.has(status))
305
+ return undefined;
306
+ const body = response.body;
307
+ if (body === undefined)
308
+ return undefined;
309
+ if (isBinaryBody(body))
310
+ return copyBinaryBody(body);
311
+ // A string body is treated as pre-serialized wire bytes regardless of
312
+ // content type: quoting it under application/json would double-encode
313
+ // routes that return JSON.stringify(...) themselves.
314
+ const serialized = typeof body === "string" ? body : stringifyJsonBody(body);
315
+ return new TextEncoder().encode(serialized);
316
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"response-parser.d.ts","sourceRoot":"","sources":["../src/response-parser.ts"],"names":[],"mappings":"AAoCA;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,WAAW,EAAE,OAAO,CAAC,WAAW,GAC/B,OAAO,CAAC,QAAQ,CAsElB"}
1
+ {"version":3,"file":"response-parser.d.ts","sourceRoot":"","sources":["../src/response-parser.ts"],"names":[],"mappings":"AAwEA;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,WAAW,EAAE,OAAO,CAAC,WAAW,GAC/B,OAAO,CAAC,QAAQ,CAuElB"}
@@ -1,12 +1,47 @@
1
1
  import { isBinaryBody } from "./binary.js";
2
2
  import { isStatusTuple } from "./constants.js";
3
+ import { InvalidResponseError } from "./errors.js";
3
4
  const BINARY_CONTENT_TYPE = "application/octet-stream";
5
+ /**
6
+ * Take ownership of caller-supplied response headers.
7
+ *
8
+ * Parsing injects a content type into the header record, so the caller's object
9
+ * must never be aliased — a generator reusing a module-level header object would
10
+ * otherwise leak the content type of one response into the next. Only the shape
11
+ * is checked here; per-value type checks stay in `normalizeResponse` so its
12
+ * distinct messages ("header names/values must be strings") are preserved.
13
+ */
14
+ function toOwnHeaderRecord(value) {
15
+ if (value === undefined)
16
+ return {};
17
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
18
+ throw new InvalidResponseError("headers must be a string record");
19
+ }
20
+ // Per-value types are `normalizeResponse`'s job (it keeps its own distinct
21
+ // message), so a non-string value is carried through here rather than
22
+ // rejected; this pass only shape-checks and detaches the caller's object.
23
+ const record = {};
24
+ Object.assign(record, value);
25
+ return record;
26
+ }
4
27
  function hasContentType(headers) {
5
28
  return Object.keys(headers).some((header) => header.toLowerCase() === "content-type");
6
29
  }
30
+ /**
31
+ * Detect the object response envelope `{ status, body, headers? }`.
32
+ *
33
+ * Detection is by shape, so a legitimate domain object carrying a numeric
34
+ * `status` next to a `body` is unwrapped instead of being delivered as the
35
+ * payload. Callers who need to return such a shape as data should nest it or
36
+ * use an explicit `[status, body]` tuple for the envelope. An object whose
37
+ * `headers` is present but not a string record is deliberately NOT an envelope
38
+ * and is delivered whole — plugins that inspect responses must use this same
39
+ * rule (see `@schmock/validation`) or they will judge an undelivered payload.
40
+ */
7
41
  function isResponseObject(value) {
8
42
  return (typeof value === "object" &&
9
43
  value !== null &&
44
+ !Array.isArray(value) &&
10
45
  "status" in value &&
11
46
  typeof value.status === "number" &&
12
47
  "body" in value &&
@@ -17,6 +52,7 @@ function isResponseObject(value) {
17
52
  function isStringRecord(value) {
18
53
  return (typeof value === "object" &&
19
54
  value !== null &&
55
+ !Array.isArray(value) &&
20
56
  Object.values(value).every((entry) => typeof entry === "string"));
21
57
  }
22
58
  /**
@@ -32,12 +68,13 @@ export function parseResponse(result, routeConfig) {
32
68
  if (isResponseObject(result)) {
33
69
  status = result.status;
34
70
  body = result.body;
35
- headers = result.headers ?? {};
71
+ headers = toOwnHeaderRecord(result.headers);
36
72
  tupleFormat = true;
37
73
  }
38
74
  else if (isStatusTuple(result)) {
39
75
  // Handle tuple response format [status, body, headers?]
40
- [status, body, headers = {}] = result;
76
+ [status, body] = result;
77
+ headers = toOwnHeaderRecord(result[2]);
41
78
  tupleFormat = true;
42
79
  }
43
80
  // Handle null/undefined responses with 204 No Content
@@ -19,6 +19,9 @@ export declare function findRoute(method: Schmock.HttpMethod, path: string, stat
19
19
  /**
20
20
  * Extract parameter values from path based on route pattern
21
21
  * Maps capture groups from regex match to parameter names
22
+ *
23
+ * Matching runs on the ENCODED path so `%2F` cannot act as a separator;
24
+ * captures are decoded afterwards, so a generator receives readable values.
22
25
  */
23
26
  export declare function extractParams(route: CompiledCallableRoute, path: string): Record<string, string>;
24
27
  //# sourceMappingURL=route-matcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../src/route-matcher.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC;IAC7B,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC;CAC7B;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,CAAC,SAAS,GACrB,GAAG,IAAI,OAAO,CAAC,iBAAiB,CAElC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,EAChD,MAAM,EAAE,qBAAqB,EAAE,GAC9B,qBAAqB,GAAG,SAAS,CAmBnC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,qBAAqB,EAC5B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB"}
1
+ {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../src/route-matcher.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC;IAC7B,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC;CAC7B;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,CAAC,SAAS,GACrB,GAAG,IAAI,OAAO,CAAC,iBAAiB,CAElC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,EAChD,MAAM,EAAE,qBAAqB,EAAE,GAC9B,qBAAqB,GAAG,SAAS,CAmBnC;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,qBAAqB,EAC5B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAcxB"}
@@ -1,4 +1,4 @@
1
- import { normalizePath } from "./constants.js";
1
+ import { decodePathSegment, normalizePath } from "./constants.js";
2
2
  export function isGeneratorFunction(gen) {
3
3
  return typeof gen === "function";
4
4
  }
@@ -26,14 +26,20 @@ export function findRoute(method, path, staticRoutes, routes) {
26
26
  /**
27
27
  * Extract parameter values from path based on route pattern
28
28
  * Maps capture groups from regex match to parameter names
29
+ *
30
+ * Matching runs on the ENCODED path so `%2F` cannot act as a separator;
31
+ * captures are decoded afterwards, so a generator receives readable values.
29
32
  */
30
33
  export function extractParams(route, path) {
31
34
  const match = path.match(route.pattern);
32
35
  if (!match)
33
36
  return {};
34
- const params = {};
35
- route.params.forEach((param, index) => {
36
- params[param] = match[index + 1];
37
- });
38
- return params;
37
+ // Object.fromEntries defines own properties, so a parameter named
38
+ // `__proto__` (or any other Object.prototype key) survives instead of being
39
+ // swallowed by the prototype setter. The prototype itself is retained so
40
+ // generators can still call params.hasOwnProperty(...).
41
+ return Object.fromEntries(route.params.map((param, index) => [
42
+ param,
43
+ decodePathSegment(match[index + 1]),
44
+ ]));
39
45
  }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type Schema = Schmock.Schema;
2
+ export type SchemaDefinition = Schmock.SchemaDefinition;
1
3
  export type HttpMethod = Schmock.HttpMethod;
2
4
  export type RouteKey = Schmock.RouteKey;
3
5
  export type ResponseBody = Schmock.ResponseBody;
@@ -17,15 +19,18 @@ export type StaticData = Schmock.StaticData;
17
19
  export type RequestRecord = Schmock.RequestRecord;
18
20
  export type ServerInfo = Schmock.ServerInfo;
19
21
  export type RouteInfo = Schmock.RouteInfo;
22
+ export type ResourceOverride = Schmock.ResourceOverride;
23
+ export type ResponseHeaderDef = Schmock.ResponseHeaderDef;
24
+ export type CrudOperationMeta = Schmock.CrudOperationMeta;
20
25
  export type SchemaGenerationContext = Schmock.SchemaGenerationContext;
21
26
  export type FakerPluginOptions = Schmock.FakerPluginOptions;
22
27
  export type ExpressAdapterOptions = Schmock.ExpressAdapterOptions;
23
28
  export type AngularAdapterOptions = Schmock.AngularAdapterOptions;
24
29
  export type OpenApiOptions = Schmock.OpenApiOptions;
30
+ export type OpenApiCallbackRequest = Schmock.OpenApiCallbackRequest;
31
+ export type OpenApiCallbackOptions = Schmock.OpenApiCallbackOptions;
25
32
  export type SeedSource = Schmock.SeedSource;
26
33
  export type SeedConfig = Schmock.SeedConfig;
27
- export type CliOptions = Schmock.CliOptions;
28
- export type CliServer = Schmock.CliServer;
29
34
  export type AdapterRequest = Schmock.AdapterRequest;
30
35
  export type AdapterResponse = Schmock.AdapterResponse;
31
36
  export type InterceptOptions = Schmock.InterceptOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;AAC5D,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;AACtD,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;AACxD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;AACtD,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;AACxD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;AACxD,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;AAC5D,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC;AACpE,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC;AACpE,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;AACtD,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;AACxD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;AACtD,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC"}