stitchkit 0.46.0 → 0.48.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 (46) hide show
  1. package/README.md +13 -3
  2. package/dist/browser/cancellation.d.ts +14 -0
  3. package/dist/browser/cancellation.d.ts.map +1 -0
  4. package/dist/browser/client-multipart.d.ts +3 -1
  5. package/dist/browser/client-multipart.d.ts.map +1 -1
  6. package/dist/browser/client.d.ts +1 -0
  7. package/dist/browser/client.d.ts.map +1 -1
  8. package/dist/browser/http.d.ts +1 -0
  9. package/dist/browser/http.d.ts.map +1 -1
  10. package/dist/cli.js +2 -2
  11. package/dist/contract/define.d.ts +67 -15
  12. package/dist/contract/define.d.ts.map +1 -1
  13. package/dist/contract/index.d.ts +1 -1
  14. package/dist/contract/index.d.ts.map +1 -1
  15. package/dist/contract/index.js +1 -1
  16. package/dist/{index-zwqty9zf.js → index-44xysy8r.js} +465 -49
  17. package/dist/{index-pwyedf7b.js → index-45dz4m51.js} +46 -0
  18. package/dist/{index-c40tkxcd.js → index-8ekq6res.js} +1 -1
  19. package/dist/{index-5s8b7z6q.js → index-ee621cmy.js} +9 -9
  20. package/dist/{index-62pqb23z.js → index-kp8xamqp.js} +1 -1
  21. package/dist/{index-w1s873ng.js → index-nrytvb30.js} +4 -3
  22. package/dist/index.js +195 -94
  23. package/dist/node.js +2 -2
  24. package/dist/observability/audit.d.ts +22 -3
  25. package/dist/observability/audit.d.ts.map +1 -1
  26. package/dist/observability/index.d.ts +1 -1
  27. package/dist/observability/index.d.ts.map +1 -1
  28. package/dist/observability/index.js +82 -15
  29. package/dist/server/context.d.ts +8 -5
  30. package/dist/server/context.d.ts.map +1 -1
  31. package/dist/server/create.d.ts.map +1 -1
  32. package/dist/server/implement.d.ts +33 -2
  33. package/dist/server/implement.d.ts.map +1 -1
  34. package/dist/server/index.d.ts +3 -3
  35. package/dist/server/index.d.ts.map +1 -1
  36. package/dist/server/index.js +28 -6
  37. package/dist/server/middleware/auth.d.ts +7 -4
  38. package/dist/server/middleware/auth.d.ts.map +1 -1
  39. package/dist/server/multipart.d.ts +13 -18
  40. package/dist/server/multipart.d.ts.map +1 -1
  41. package/dist/server/openapi.d.ts.map +1 -1
  42. package/dist/server/types.d.ts +48 -15
  43. package/dist/server/types.d.ts.map +1 -1
  44. package/dist/tools.js +171 -74
  45. package/llms-full.txt +411 -67
  46. package/package.json +1 -1
@@ -114,6 +114,14 @@ function parseTrailingWildcard(path) {
114
114
  return wildcard;
115
115
  }
116
116
 
117
+ // src/internal/safe-json.ts
118
+ function isUnsafeKey(key) {
119
+ return key === "__proto__";
120
+ }
121
+ function safeJsonParse(text) {
122
+ return JSON.parse(text, (key, value) => isUnsafeKey(key) ? undefined : value);
123
+ }
124
+
117
125
  // src/contract/define.ts
118
126
  function mergeMeta(contractMeta, endpointMeta) {
119
127
  if (!contractMeta)
@@ -260,14 +268,6 @@ function validateDeclaredOutput(schema, data, onStripped) {
260
268
  return validateHandlerOutput(schema, data, onStripped);
261
269
  }
262
270
 
263
- // src/internal/safe-json.ts
264
- function isUnsafeKey(key) {
265
- return key === "__proto__";
266
- }
267
- function safeJsonParse(text) {
268
- return JSON.parse(text, (key, value) => isUnsafeKey(key) ? undefined : value);
269
- }
270
-
271
271
  // src/server/request.ts
272
272
  function generateTraceId() {
273
273
  return `${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
@@ -454,4 +454,4 @@ function wrapInRequestContext(handler, options = {}) {
454
454
  };
455
455
  }
456
456
 
457
- export { __require, parseTrailingWildcard, mergeMeta, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, typedEntries, isRecord, callRuntimeHandler, bytesToBase64Url, base64UrlToBytes, formatZodError, zodIssues, errorCode, recordedErrorMessage, normalizeError, validateDeclaredOutput, isUnsafeKey, safeJsonParse, generateTraceId, resolveTraceId, resolveSocketIp, extractIp, getClientInfo, parseQueryParams, createTraceContext, parseTraceparent, formatTraceparent, resolveTraceContext, childSpan, resolvePropagationContext, runWithRequestContext, getRequestContext, getTraceId, getUserId, setRequestUser, setRequestEndpoint, setRequestDimensions, setRequestError, wrapInRequestContext };
457
+ export { __require, parseTrailingWildcard, isUnsafeKey, safeJsonParse, mergeMeta, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, typedEntries, isRecord, callRuntimeHandler, bytesToBase64Url, base64UrlToBytes, formatZodError, zodIssues, errorCode, recordedErrorMessage, normalizeError, validateDeclaredOutput, generateTraceId, resolveTraceId, resolveSocketIp, extractIp, getClientInfo, parseQueryParams, createTraceContext, parseTraceparent, formatTraceparent, resolveTraceContext, childSpan, resolvePropagationContext, runWithRequestContext, getRequestContext, getTraceId, getUserId, setRequestUser, setRequestEndpoint, setRequestDimensions, setRequestError, wrapInRequestContext };
@@ -15,7 +15,7 @@ import {
15
15
  runWithRequestContext,
16
16
  safeJsonParse,
17
17
  validateDeclaredOutput
18
- } from "./index-5s8b7z6q.js";
18
+ } from "./index-ee621cmy.js";
19
19
 
20
20
  // src/tools/coerce.ts
21
21
  import { z } from "zod";
@@ -6,7 +6,7 @@ import {
6
6
  isUnsafeKey,
7
7
  safeJsonParse,
8
8
  unauthorized
9
- } from "./index-5s8b7z6q.js";
9
+ } from "./index-ee621cmy.js";
10
10
 
11
11
  // src/server/middleware/cookies.ts
12
12
  function parseCookies(header) {
@@ -151,7 +151,7 @@ function extractToken(req, cookieName) {
151
151
  function createAuthHook(config) {
152
152
  const onAnonymous = config.onAnonymous ?? (() => unauthorized());
153
153
  const onForbidden = config.onForbidden ?? (() => forbidden());
154
- return async (ctx, endpoint) => {
154
+ async function auth(ctx, endpoint) {
155
155
  const identity = ctx.source === "http" ? await config.resolve(ctx) : await config.resolveFromContext?.(ctx) ?? null;
156
156
  config.inject?.(ctx, identity);
157
157
  const scope = endpoint.scope ?? config.defaultScope;
@@ -171,7 +171,8 @@ function createAuthHook(config) {
171
171
  return;
172
172
  if (!await rule(identity, ctx))
173
173
  onForbidden();
174
- };
174
+ }
175
+ return auth;
175
176
  }
176
177
  function createBearerResolver(config) {
177
178
  return async (req) => {
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  parseTrailingWildcard,
18
18
  rateLimited,
19
19
  unauthorized
20
- } from "./index-pwyedf7b.js";
20
+ } from "./index-45dz4m51.js";
21
21
  import {
22
22
  isRecord,
23
23
  mapObject,
@@ -25,6 +25,63 @@ import {
25
25
  typedEntries
26
26
  } from "./index-tss6bk5c.js";
27
27
 
28
+ // src/browser/cancellation.ts
29
+ class RequestCancellationError extends Error {
30
+ cause;
31
+ constructor(cause) {
32
+ super(cause === "caller" ? "Request was aborted" : "Request timed out");
33
+ this.cause = cause;
34
+ this.name = "RequestCancellationError";
35
+ }
36
+ }
37
+ function createRequestCancellation(caller, timeoutMs) {
38
+ if (!caller && timeoutMs === undefined) {
39
+ return { signal: undefined, run: (operation) => operation() };
40
+ }
41
+ const controller = new AbortController;
42
+ let cause;
43
+ let timer;
44
+ const abortFromCaller = () => {
45
+ if (cause)
46
+ return;
47
+ cause = "caller";
48
+ controller.abort(caller?.reason);
49
+ };
50
+ if (caller?.aborted)
51
+ abortFromCaller();
52
+ else
53
+ caller?.addEventListener("abort", abortFromCaller, { once: true });
54
+ if (timeoutMs !== undefined) {
55
+ timer = setTimeout(() => {
56
+ if (cause)
57
+ return;
58
+ cause = "timeout";
59
+ controller.abort(new DOMException("Request timed out", "TimeoutError"));
60
+ }, timeoutMs);
61
+ }
62
+ return {
63
+ signal: controller.signal,
64
+ async run(operation) {
65
+ try {
66
+ if (cause === "caller")
67
+ throw cancellationError(cause);
68
+ return await operation(controller.signal);
69
+ } catch (error) {
70
+ if (cause)
71
+ throw cancellationError(cause);
72
+ throw error;
73
+ } finally {
74
+ if (timer !== undefined)
75
+ clearTimeout(timer);
76
+ caller?.removeEventListener("abort", abortFromCaller);
77
+ }
78
+ }
79
+ };
80
+ }
81
+ function cancellationError(cause) {
82
+ return new RequestCancellationError(cause);
83
+ }
84
+
28
85
  // src/browser/client-multipart.ts
29
86
  function isFileDescriptor(value) {
30
87
  return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
@@ -43,6 +100,35 @@ function appendFormFields(formData, values, skipKeys) {
43
100
  formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
44
101
  }
45
102
  }
103
+ function buildMultipartForm(descriptor, values) {
104
+ const formData = new FormData;
105
+ const fileFields = new Set(Object.keys(descriptor.files));
106
+ for (const [field, policy] of Object.entries(descriptor.files)) {
107
+ const value = values[field];
108
+ if (value === undefined) {
109
+ if (policy.required !== false)
110
+ throw new Error(`Missing multipart file field: ${field}`);
111
+ continue;
112
+ }
113
+ if (policy.multiple === true) {
114
+ if (!Array.isArray(value) || value.length === 0) {
115
+ throw new Error(`Multipart file field "${field}" must be a non-empty array`);
116
+ }
117
+ for (const file of value) {
118
+ if (!isMultipartFile(file)) {
119
+ throw new Error(`Invalid multipart file field: ${field}`);
120
+ }
121
+ appendMultipartFile(formData, field, file);
122
+ }
123
+ continue;
124
+ }
125
+ if (!isMultipartFile(value))
126
+ throw new Error(`Invalid multipart file field: ${field}`);
127
+ appendMultipartFile(formData, field, value);
128
+ }
129
+ appendFormFields(formData, values, fileFields);
130
+ return formData;
131
+ }
46
132
 
47
133
  // src/internal/http-input.ts
48
134
  function inputIsQuery(method) {
@@ -314,7 +400,7 @@ function createHttpClient(config) {
314
400
  const client = ky.create({
315
401
  prefix: config.baseUrl,
316
402
  credentials: config.credentials ?? "include",
317
- timeout: config.timeout ?? 30000,
403
+ timeout: false,
318
404
  retry: {
319
405
  limit: config.retry?.limit ?? 2,
320
406
  methods: config.retry?.methods ?? ["get"],
@@ -360,8 +446,10 @@ function createHttpClient(config) {
360
446
  }
361
447
  });
362
448
  async function request(method, url, data, options = {}) {
449
+ const cancellation = createRequestCancellation(options.signal, options.timeout ?? config.timeout ?? 30000);
363
450
  const kyOptions = {
364
- timeout: options.timeout
451
+ timeout: false,
452
+ signal: cancellation.signal
365
453
  };
366
454
  if (options.params) {
367
455
  const searchParams = new URLSearchParams;
@@ -385,31 +473,33 @@ function createHttpClient(config) {
385
473
  kyOptions.json = data;
386
474
  }
387
475
  try {
388
- if (options.responseType === "blob") {
389
- return transportResult(await client[method](url, kyOptions).blob());
390
- }
391
- if (options.responseType === "response") {
392
- return transportResult(await client[method](url, kyOptions));
393
- }
394
- const response = await client[method](url, kyOptions);
395
- if (options.responseType === "void") {
396
- const text = await response.text();
397
- if (text.length > 0) {
398
- throw new Error("Server returned data for an endpoint with no output contract");
476
+ return await cancellation.run(async () => {
477
+ if (options.responseType === "blob") {
478
+ return transportResult(await client[method](url, kyOptions).blob());
399
479
  }
400
- }
401
- if (options.responseType === "void" || response.status === 204 || response.headers.get("content-length") === "0") {
402
- return transportResult(undefined);
403
- }
404
- return await response.json();
480
+ if (options.responseType === "response") {
481
+ return transportResult(await client[method](url, kyOptions));
482
+ }
483
+ const response = await client[method](url, kyOptions);
484
+ if (options.responseType === "void") {
485
+ const text = await response.text();
486
+ if (text.length > 0) {
487
+ throw new Error("Server returned data for an endpoint with no output contract");
488
+ }
489
+ }
490
+ if (options.responseType === "void" || response.status === 204 || response.headers.get("content-length") === "0") {
491
+ return transportResult(undefined);
492
+ }
493
+ return await response.json();
494
+ });
405
495
  } catch (error) {
496
+ if (error instanceof RequestCancellationError) {
497
+ throw new ApiError(error.cause === "caller" ? "REQUEST_ABORTED" : "REQUEST_TIMEOUT", 0, undefined, error.message);
498
+ }
406
499
  if (ApiError.is(error))
407
500
  throw error;
408
- const isAbort = error instanceof Error && error.name === "AbortError";
409
- if (!isAbort) {
410
- emit({ type: "network_error" });
411
- }
412
- const response = !isAbort && isHTTPError(error) ? error.response : undefined;
501
+ emit({ type: "network_error" });
502
+ const response = isHTTPError(error) ? error.response : undefined;
413
503
  const status = response?.status ?? 0;
414
504
  const msg = error instanceof Error ? error.message : undefined;
415
505
  throw new ApiError("UNKNOWN_ERROR", status, msg ? { message: msg } : undefined, undefined, undefined, responseTraceId(response));
@@ -488,11 +578,11 @@ function contractEndpointMatchers(contract, endpointNames, contractConfig) {
488
578
  }
489
579
  function createClient(contract, configOrClient, contractConfig) {
490
580
  const client = {};
491
- const makeMethod = isHttpAdapter(configOrClient) ? (endpoint) => createHttpMethod(endpoint, contract.meta.prefix, configOrClient, contractConfig) : (endpoint) => createFetchMethod(endpoint, contract.meta.prefix, configOrClient, contractConfig);
581
+ const makeExecutor = isHttpAdapter(configOrClient) ? (endpoint) => createHttpExecutor(endpoint, contract.meta.prefix, configOrClient, contractConfig) : (endpoint) => createFetchExecutor(endpoint, contract.meta.prefix, configOrClient, contractConfig);
492
582
  for (const [key, endpoint] of typedEntries(contract.endpoints)) {
493
583
  if (endpoint.expose && !endpoint.expose.includes("HTTP"))
494
584
  continue;
495
- setClientMethod(client, key, makeMethod(endpoint));
585
+ setClientMethod(client, key, createEndpointMethod(endpoint, makeExecutor(endpoint), contractConfig));
496
586
  }
497
587
  return client;
498
588
  }
@@ -533,98 +623,109 @@ function isHttpAdapter(value) {
533
623
  function setClientMethod(target, key, method) {
534
624
  target[key] = method;
535
625
  }
536
- function createHttpMethod(endpoint, prefix, client, config) {
626
+ function createEndpointMethod(endpoint, execute, contractConfig) {
627
+ const hasScopedArguments = (contractConfig?.stripPrefixKeys?.length ?? 0) > 0;
628
+ if (endpointHasArguments(endpoint) || hasScopedArguments) {
629
+ const method2 = (requestArgs) => execute(readClientRequestArgs(requestArgs), undefined);
630
+ return Object.assign(method2, {
631
+ withOptions: (requestArgs, options) => execute(readClientRequestArgs(requestArgs), readClientRequestOptions(options))
632
+ });
633
+ }
634
+ const method = () => execute({}, undefined);
635
+ return Object.assign(method, {
636
+ withOptions: (options) => execute({}, readClientRequestOptions(options))
637
+ });
638
+ }
639
+ function createHttpExecutor(endpoint, prefix, client, config) {
537
640
  const httpMethod = endpoint.method.toLowerCase();
538
- return (...args) => {
539
- const firstArg = args[0] ?? {};
540
- const plan = planClientRequest(endpoint, prefix, firstArg, config);
641
+ return (requestArgs, options) => {
642
+ const plan = planClientRequest(endpoint, prefix, requestArgs, config);
541
643
  if (endpoint.multipart) {
542
- const file = firstArg[endpoint.multipart];
543
- if (!isMultipartFile(file)) {
544
- throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
545
- }
546
644
  if (httpMethod === "get" || httpMethod === "head" || httpMethod === "delete") {
547
645
  throw new Error(`Multipart endpoint ${endpoint.method} ${endpoint.path} must be POST / PUT / PATCH`);
548
646
  }
549
- const formData = new FormData;
550
- appendMultipartFile(formData, endpoint.multipart, file);
551
- appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
552
- return withOutput(endpoint, client[httpMethod](plan.relativeUrl, formData, withTimeout(undefined, endpoint)));
647
+ const formData = buildMultipartForm(endpoint.multipart, plan.remainingArgs);
648
+ return withOutput(endpoint, client[httpMethod](plan.relativeUrl, formData, withTimeout(options, endpoint)));
553
649
  }
554
650
  if (httpMethod === "get" || httpMethod === "head") {
555
- return withOutput(endpoint, client[httpMethod](plan.relativeUrl, withTimeout(undefined, endpoint)));
651
+ return withOutput(endpoint, client[httpMethod](plan.relativeUrl, withTimeout(options, endpoint)));
556
652
  }
557
653
  if (httpMethod === "delete") {
558
- return withOutput(endpoint, client.delete(plan.relativeUrl, withTimeout(undefined, endpoint)));
654
+ return withOutput(endpoint, client.delete(plan.relativeUrl, withTimeout(options, endpoint)));
559
655
  }
560
- return withOutput(endpoint, client[httpMethod](plan.relativeUrl, Object.keys(plan.remainingArgs).length > 0 ? plan.remainingArgs : undefined, withTimeout(undefined, endpoint)));
656
+ return withOutput(endpoint, client[httpMethod](plan.relativeUrl, Object.keys(plan.remainingArgs).length > 0 ? plan.remainingArgs : undefined, withTimeout(options, endpoint)));
561
657
  };
562
658
  }
563
- function createFetchMethod(endpoint, prefix, config, contractConfig) {
564
- return async (args) => {
565
- const plan = planClientRequest(endpoint, prefix, args ?? {}, contractConfig);
659
+ function createFetchExecutor(endpoint, prefix, config, contractConfig) {
660
+ return async (requestArgs, options) => {
661
+ const plan = planClientRequest(endpoint, prefix, requestArgs, contractConfig);
566
662
  const url = joinClientBaseUrl(config.baseUrl, plan.relativeUrl);
567
663
  const headers = {
568
664
  Accept: "application/json",
569
665
  ...typeof config.headers === "function" ? config.headers() : config.headers
570
666
  };
571
- const signal = endpoint.timeout !== undefined ? AbortSignal.timeout(endpoint.timeout) : undefined;
572
- const hasBody = endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && args;
667
+ const cancellation = createRequestCancellation(options?.signal, endpoint.timeout ?? config.timeout ?? 30000);
668
+ const hasBody = endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && Object.keys(plan.remainingArgs).length > 0;
573
669
  if (hasBody)
574
670
  headers["Content-Type"] = "application/json";
575
- if (endpoint.multipart && args) {
576
- const file = args[endpoint.multipart];
577
- if (!isMultipartFile(file)) {
578
- throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
579
- }
580
- const formData = new FormData;
581
- appendMultipartFile(formData, endpoint.multipart, file);
582
- appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
583
- const res2 = await fetch(url, {
584
- method: endpoint.method,
585
- headers,
586
- credentials: config.credentials,
587
- body: formData,
588
- signal
589
- });
590
- if (!res2.ok) {
591
- await throwForErrorResponse(res2, config, null);
592
- }
593
- if (endpoint.rawResponse)
594
- return res2;
595
- if (!endpoint.output) {
596
- const text = await res2.text();
597
- if (text.length > 0) {
598
- throw new Error("Server returned data for an endpoint with no output contract");
671
+ try {
672
+ return await cancellation.run(async (signal) => {
673
+ const body = endpoint.multipart ? buildMultipartForm(endpoint.multipart, plan.remainingArgs) : hasBody ? JSON.stringify(plan.remainingArgs) : undefined;
674
+ const res = await fetch(url, {
675
+ method: endpoint.method,
676
+ headers,
677
+ credentials: config.credentials,
678
+ signal,
679
+ ...body !== undefined && { body }
680
+ });
681
+ if (!res.ok) {
682
+ await throwForErrorResponse(res, config, { error: res.statusText });
599
683
  }
600
- return;
601
- }
602
- return endpoint.output.parse(await res2.json());
603
- }
604
- const res = await fetch(url, {
605
- method: endpoint.method,
606
- headers,
607
- credentials: config.credentials,
608
- signal,
609
- ...hasBody && {
610
- body: JSON.stringify(plan.remainingArgs)
611
- }
612
- });
613
- if (!res.ok) {
614
- await throwForErrorResponse(res, config, { error: res.statusText });
615
- }
616
- if (endpoint.rawResponse)
617
- return res;
618
- if (!endpoint.output) {
619
- const text = await res.text();
620
- if (text.length > 0) {
621
- throw new Error("Server returned data for an endpoint with no output contract");
684
+ if (endpoint.rawResponse)
685
+ return res;
686
+ if (!endpoint.output) {
687
+ const text = await res.text();
688
+ if (text.length > 0) {
689
+ throw new Error("Server returned data for an endpoint with no output contract");
690
+ }
691
+ return;
692
+ }
693
+ return endpoint.output.parse(await res.json());
694
+ });
695
+ } catch (error) {
696
+ if (error instanceof RequestCancellationError) {
697
+ throw new ApiError(error.cause === "caller" ? "REQUEST_ABORTED" : "REQUEST_TIMEOUT", 0, undefined, error.message);
622
698
  }
623
- return;
699
+ if (ApiError.is(error))
700
+ throw error;
701
+ const message = error instanceof Error ? error.message : undefined;
702
+ throw new ApiError("UNKNOWN_ERROR", 0, message ? { message } : undefined, message);
624
703
  }
625
- return endpoint.output.parse(await res.json());
626
704
  };
627
705
  }
706
+ function endpointHasArguments(endpoint) {
707
+ return Boolean(endpoint.params || endpoint.input || endpoint.multipart);
708
+ }
709
+ function readClientRequestArgs(requestArgs) {
710
+ if (requestArgs !== undefined && !isRecord(requestArgs)) {
711
+ throw new TypeError("Endpoint arguments must be an object");
712
+ }
713
+ return requestArgs ?? {};
714
+ }
715
+ function readClientRequestOptions(value) {
716
+ if (!isRecord(value))
717
+ throw new TypeError("Client request options must be an object");
718
+ const signal = value.signal;
719
+ if (signal === undefined)
720
+ return {};
721
+ if (!isAbortSignal(signal)) {
722
+ throw new TypeError("Client request signal must be an AbortSignal");
723
+ }
724
+ return { signal };
725
+ }
726
+ function isAbortSignal(value) {
727
+ return typeof value === "object" && value !== null && "aborted" in value && typeof value.aborted === "boolean" && "addEventListener" in value && typeof value.addEventListener === "function" && "removeEventListener" in value && typeof value.removeEventListener === "function";
728
+ }
628
729
  async function throwForErrorResponse(res, config, fallbackBody) {
629
730
  const body = await res.json().catch(() => fallbackBody);
630
731
  config.onError?.(res.status, body);
package/dist/node.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  createImplement,
5
5
  createSocketIOServer,
6
6
  implement
7
- } from "./index-zwqty9zf.js";
7
+ } from "./index-44xysy8r.js";
8
8
  import"./index-r1qp4rve.js";
9
9
  import {
10
10
  AppError,
@@ -15,7 +15,7 @@ import {
15
15
  notFound,
16
16
  rateLimited,
17
17
  unauthorized
18
- } from "./index-5s8b7z6q.js";
18
+ } from "./index-ee621cmy.js";
19
19
  // src/server/node.ts
20
20
  import { serve } from "srvx";
21
21
  async function serveNode(config) {
@@ -5,15 +5,30 @@ import { type SanitizeOptions } from './sanitize';
5
5
  /** One isolated RequestEvent sink and its sanitisation policy. */
6
6
  export interface RequestEventSinkConfig {
7
7
  /**
8
- * The sink — persists one audit event. It runs fire-and-forget and its own
9
- * errors are swallowed, so a slow or failing write never blocks or breaks the
10
- * request it observes. Keep it asynchronous and self-contained.
8
+ * The sink — persists one audit event. It runs fire-and-forget; failures are
9
+ * reported through `onSinkError` without breaking the observed work.
11
10
  */
12
11
  write: (event: RequestEvent) => void | Promise<void>;
13
12
  /** Keep only events for which this returns `true`. Default: keep every event. */
14
13
  filter?: (event: RequestEvent) => boolean;
15
14
  /** Payload sanitisation tuning — passed through to `sanitizePayload`. */
16
15
  sanitize?: SanitizeOptions;
16
+ /** Maximum writes concurrently awaiting settlement. Default: 1000. */
17
+ maxPending?: number;
18
+ /** Observe failed filtering, projection or persistence. */
19
+ onSinkError?: (failure: SinkError) => void | Promise<void>;
20
+ /** Observe events rejected by bounded or closed admission. */
21
+ onDrop?: (drop: SinkDrop) => void | Promise<void>;
22
+ }
23
+ export type SinkDropReason = 'capacity' | 'closed';
24
+ export interface SinkError {
25
+ error: unknown;
26
+ event?: RequestEvent;
27
+ }
28
+ export interface SinkDrop {
29
+ reason: SinkDropReason;
30
+ event: RequestEvent;
31
+ pending: number;
17
32
  }
18
33
  export interface RequestObservabilityConfig extends RequestEventSinkConfig {
19
34
  /** Clone and record JSON request bodies. Default false. */
@@ -39,6 +54,10 @@ export interface HttpRequestObserver {
39
54
  export interface Observability {
40
55
  request?: HttpRequestObserver;
41
56
  toolCall: ToolCallHooks;
57
+ /** Wait for events admitted before this call. */
58
+ flush(): Promise<void>;
59
+ /** Stop admission and drain every previously accepted event. Idempotent. */
60
+ close(): Promise<void>;
42
61
  }
43
62
  export declare function createObservability(config: ObservabilityConfig): Observability;
44
63
  //# sourceMappingURL=audit.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/observability/audit.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAe,KAAK,eAAe,EAAmB,MAAM,YAAY,CAAC;AAGhF,kEAAkE;AAClE,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;IAC1C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA2B,SAAQ,sBAAsB;IACxE,2DAA2D;IAC3D,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,0BAA0B,CAAC;IACrC,KAAK,CAAC,EAAE,sBAAsB,CAAC;CAChC;AAED,gEAAgE;AAChE,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5B;AAED,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,qBAAqB,GAAG,IAAI,CAAC;CACnD;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,aAAa,CAAC;CACzB;AAkFD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,aAAa,CAwH9E"}
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/observability/audit.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAe,KAAK,eAAe,EAAmB,MAAM,YAAY,CAAC;AAGhF,kEAAkE;AAClE,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;IAC1C,yEAAyE;IACzE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,8DAA8D;IAC9D,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,0BAA2B,SAAQ,sBAAsB;IACxE,2DAA2D;IAC3D,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,0BAA0B,CAAC;IACrC,KAAK,CAAC,EAAE,sBAAsB,CAAC;CAChC;AAED,gEAAgE;AAChE,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5B;AAED,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,qBAAqB,GAAG,IAAI,CAAC;CACnD;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,aAAa,CAAC;IACxB,iDAAiD;IACjD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,4EAA4E;IAC5E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAuKD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,aAAa,CAkI9E"}
@@ -6,7 +6,7 @@
6
6
  * framework-owned HTTP completion plus canonical tool hooks into independent
7
7
  * sinks.
8
8
  */
9
- export { createObservability, type HttpRequestCompletion, type HttpRequestObserver, type Observability, type ObservabilityConfig, type RequestEventSinkConfig, type RequestObservabilityConfig, } from './audit';
9
+ export { createObservability, type HttpRequestCompletion, type HttpRequestObserver, type Observability, type ObservabilityConfig, type RequestEventSinkConfig, type RequestObservabilityConfig, type SinkDrop, type SinkDropReason, type SinkError, } from './audit';
10
10
  export { getRequestContext, getTraceId, getUserId, type RequestContext, runWithRequestContext, setRequestDimensions, setRequestEndpoint, setRequestError, setRequestUser, type WrapRequestContextOptions, wrapInRequestContext, } from './context';
11
11
  export type { RequestEvent } from './event';
12
12
  export { type JsonValue, measureSize, redact, type SanitizeOptions, type SizeMeasure, sanitizePayload, truncatePreview, } from './sanitize';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/observability/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,0BAA0B,GAChC,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,SAAS,EACT,KAAK,cAAc,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,KAAK,yBAAyB,EAC9B,oBAAoB,GACrB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EACL,KAAK,SAAS,EACd,WAAW,EACX,MAAM,EACN,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,yBAAyB,EACzB,mBAAmB,EACnB,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/observability/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,0BAA0B,EAC/B,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,SAAS,GACf,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,SAAS,EACT,KAAK,cAAc,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,KAAK,yBAAyB,EAC9B,oBAAoB,GACrB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EACL,KAAK,SAAS,EACd,WAAW,EACX,MAAM,EACN,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,yBAAyB,EACzB,mBAAmB,EACnB,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC"}