@glasstrace/sdk 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +55 -20
  2. package/dist/async-context/index.cjs.map +1 -1
  3. package/dist/async-context/index.js +2 -2
  4. package/dist/{capture-error-CRQSqKgd.d.cts → capture-error-03qDnC5v.d.cts} +2 -2
  5. package/dist/{capture-error-CqCbjoMq.d.ts → capture-error-CAfFUyIU.d.ts} +2 -2
  6. package/dist/{chunk-6NKMAKMI.js → chunk-6ST4QV7T.js} +3 -3
  7. package/dist/{chunk-BN5DVIUO.js → chunk-7LE2O4ZJ.js} +12 -7
  8. package/dist/{chunk-BN5DVIUO.js.map → chunk-7LE2O4ZJ.js.map} +1 -1
  9. package/dist/{chunk-J3ZCVE67.js → chunk-F7A3QXCT.js} +2 -2
  10. package/dist/{chunk-M3ZTG4P5.js → chunk-HMEHYSTS.js} +2 -2
  11. package/dist/{chunk-4YRYR32D.js → chunk-LQZRGBN5.js} +2 -2
  12. package/dist/{chunk-ZIL2IY4C.js → chunk-MP3QNDXQ.js} +2 -2
  13. package/dist/{chunk-FFNGE5RR.js → chunk-VMK2G6QR.js} +2 -2
  14. package/dist/{chunk-CZQN6G4I.js → chunk-VWZMG3W2.js} +10 -10
  15. package/dist/{chunk-KBHGPQZ2.js → chunk-XMD5OYD6.js} +2 -2
  16. package/dist/{chunk-V75ZB4G4.js → chunk-ZIYT2Y4B.js} +3 -3
  17. package/dist/cli/init.cjs +4 -4
  18. package/dist/cli/init.cjs.map +1 -1
  19. package/dist/cli/init.js +7 -7
  20. package/dist/cli/mcp-add.cjs +1 -1
  21. package/dist/cli/mcp-add.cjs.map +1 -1
  22. package/dist/cli/mcp-add.js +3 -3
  23. package/dist/cli/uninit.js +3 -3
  24. package/dist/cli/upgrade-instructions.cjs +1 -1
  25. package/dist/cli/upgrade-instructions.js +3 -3
  26. package/dist/cli/validate.cjs.map +1 -1
  27. package/dist/cli/validate.js +2 -2
  28. package/dist/{correlation-id-CFpyJsKv.d.cts → correlation-id-CZ2bstzA.d.cts} +1 -1
  29. package/dist/{correlation-id-DPXyY9-3.d.ts → correlation-id-YcfcqOru.d.ts} +1 -1
  30. package/dist/edge-entry.cjs.map +1 -1
  31. package/dist/edge-entry.d.cts +2 -2
  32. package/dist/edge-entry.d.ts +2 -2
  33. package/dist/edge-entry.js +4 -4
  34. package/dist/index.cjs +32 -18
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/{index.d-D-jdiIPU.d.cts → index.d-BQIJ5Dvc.d.cts} +44 -9
  37. package/dist/{index.d-D-jdiIPU.d.ts → index.d-BQIJ5Dvc.d.ts} +44 -9
  38. package/dist/index.d.cts +4 -4
  39. package/dist/index.d.ts +4 -4
  40. package/dist/index.js +19 -14
  41. package/dist/index.js.map +1 -1
  42. package/dist/middleware/index.cjs.map +1 -1
  43. package/dist/middleware/index.js +2 -2
  44. package/dist/node-entry.cjs +5 -5
  45. package/dist/node-entry.cjs.map +1 -1
  46. package/dist/node-entry.d.cts +3 -3
  47. package/dist/node-entry.d.ts +3 -3
  48. package/dist/node-entry.js +7 -7
  49. package/dist/node-subpath.cjs.map +1 -1
  50. package/dist/node-subpath.d.cts +1 -1
  51. package/dist/node-subpath.d.ts +1 -1
  52. package/dist/node-subpath.js +3 -3
  53. package/dist/{source-map-uploader-K67LTPBG.js → source-map-uploader-NUONOEJG.js} +3 -3
  54. package/dist/trpc/index.cjs.map +1 -1
  55. package/dist/trpc/index.js +1 -1
  56. package/package.json +1 -1
  57. /package/dist/{chunk-6NKMAKMI.js.map → chunk-6ST4QV7T.js.map} +0 -0
  58. /package/dist/{chunk-J3ZCVE67.js.map → chunk-F7A3QXCT.js.map} +0 -0
  59. /package/dist/{chunk-M3ZTG4P5.js.map → chunk-HMEHYSTS.js.map} +0 -0
  60. /package/dist/{chunk-4YRYR32D.js.map → chunk-LQZRGBN5.js.map} +0 -0
  61. /package/dist/{chunk-ZIL2IY4C.js.map → chunk-MP3QNDXQ.js.map} +0 -0
  62. /package/dist/{chunk-FFNGE5RR.js.map → chunk-VMK2G6QR.js.map} +0 -0
  63. /package/dist/{chunk-CZQN6G4I.js.map → chunk-VWZMG3W2.js.map} +0 -0
  64. /package/dist/{chunk-KBHGPQZ2.js.map → chunk-XMD5OYD6.js.map} +0 -0
  65. /package/dist/{chunk-V75ZB4G4.js.map → chunk-ZIYT2Y4B.js.map} +0 -0
  66. /package/dist/{source-map-uploader-K67LTPBG.js.map → source-map-uploader-NUONOEJG.js.map} +0 -0
@@ -227,19 +227,54 @@ declare const SIDE_EFFECT_OPERATION_KINDS: readonly ["email", "calendar_link", "
227
227
  */
228
228
  type SideEffectOperationKind = (typeof SIDE_EFFECT_OPERATION_KINDS)[number];
229
229
  /**
230
- * Allowlisted semantic field keys for `recordSideEffect()` evidence.
230
+ * Stable-core semantic field keys for `recordSideEffect()` evidence.
231
231
  *
232
- * Only fields named here may be attached to a side-effect operation
233
- * summary. Any other key is silently dropped at the SDK boundary and
234
- * counted under the `unsupported_key` omission reason.
232
+ * These seven keys have specialized value validators (BCP-47 for
233
+ * `locale`, IANA for `timezone`, compact token for the rest) and are
234
+ * the closed set of names that can never be removed without a
235
+ * coordinated wire-breaking change. Pattern-admitted keys (see
236
+ * {@link SIDE_EFFECT_SEMANTIC_FIELD_OPEN_PATTERN}) live alongside
237
+ * these and are validated by suffix routing.
235
238
  */
236
- declare const SIDE_EFFECT_SEMANTIC_FIELD_KEYS: readonly ["templateKey", "providerOperation", "role", "locale", "timezone", "status", "phase", "recipientClass", "participantCount", "activeParticipantCount"];
239
+ declare const SIDE_EFFECT_SEMANTIC_FIELD_STABLE_CORE_KEYS: readonly ["templateKey", "providerOperation", "role", "locale", "timezone", "status", "phase"];
237
240
  /**
238
- * One of the allowlisted side-effect semantic field keys.
241
+ * One of the stable-core semantic field keys (compile-time literal
242
+ * union). Consumers who want autocomplete for the closed stable-core
243
+ * set import this narrower type; consumers who accept the wider
244
+ * named-pattern admission shape use {@link SideEffectSemanticFieldKey}.
239
245
  *
240
- * @see {@link SIDE_EFFECT_SEMANTIC_FIELD_KEYS}
246
+ * @see {@link SIDE_EFFECT_SEMANTIC_FIELD_STABLE_CORE_KEYS}
241
247
  */
242
- type SideEffectSemanticFieldKey = (typeof SIDE_EFFECT_SEMANTIC_FIELD_KEYS)[number];
248
+ type SideEffectSemanticFieldStableCoreKey = (typeof SIDE_EFFECT_SEMANTIC_FIELD_STABLE_CORE_KEYS)[number];
249
+ /**
250
+ * Runtime guard for the semantic field key admission contract.
251
+ *
252
+ * Returns `true` when `key` is non-empty, no longer than
253
+ * {@link MAX_SIDE_EFFECT_SEMANTIC_FIELD_KEY_LENGTH}, and either one
254
+ * of the seven stable-core keys or matches the open-pattern regex.
255
+ * Use this guard at any runtime call-site that needs to validate a
256
+ * producer-supplied key before emission; for compile-time
257
+ * autocomplete on the stable-core subset, import
258
+ * {@link SideEffectSemanticFieldStableCoreKey}.
259
+ *
260
+ * Note: the length cap is part of the admission contract, not a
261
+ * separate SDK runtime concern. Consumers using this guard see the
262
+ * same decision the SDK uses at `recordSideEffect()` emission.
263
+ */
264
+ declare function isSideEffectSemanticFieldKey(key: string): boolean;
265
+ /**
266
+ * Any admissible semantic field key — the stable-core literal union
267
+ * plus any string matching the open-pattern regex.
268
+ *
269
+ * Note: TypeScript collapses `<literal-union> | string` to `string`
270
+ * at the type level (the `string` member subsumes the literal arm),
271
+ * so compile-time narrowing is intentionally relaxed at this
272
+ * surface. Runtime admission is enforced by
273
+ * {@link isSideEffectSemanticFieldKey}. Consumers who want
274
+ * compile-time stable-core autocomplete should import
275
+ * {@link SideEffectSemanticFieldStableCoreKey} instead.
276
+ */
277
+ type SideEffectSemanticFieldKey = SideEffectSemanticFieldStableCoreKey | string;
243
278
  /**
244
279
  * Allowlisted reasons a side-effect value may be omitted from the
245
280
  * emitted summary.
@@ -281,4 +316,4 @@ declare const SIDE_EFFECT_OPERATION_PHASES: readonly ["request", "post_response"
281
316
  */
282
317
  type SideEffectOperationPhase = (typeof SIDE_EFFECT_OPERATION_PHASES)[number];
283
318
 
284
- export { type AnonApiKey as A, type CaptureConfig as C, type GlasstraceEnvVars as G, type ImportGraphPayload as I, type SideEffectOperationKind as S, type SideEffectOperationStatus as a, type SideEffectOperationPhase as b, type SideEffectSemanticFieldKey as c, deriveSessionId as d, type SideEffectOmissionReason as e, type SdkDiagnosticCode as f, type SessionId as g, type GlasstraceOptions as h, type SdkInitResponse as i, type SdkHealthReport as j, type SourceMapUploadResponse as k, type SourceMapManifestResponse as l };
319
+ export { type AnonApiKey as A, type CaptureConfig as C, type GlasstraceEnvVars as G, type ImportGraphPayload as I, type SideEffectOperationKind as S, type SideEffectOperationStatus as a, type SideEffectOperationPhase as b, type SideEffectSemanticFieldKey as c, deriveSessionId as d, type SideEffectOmissionReason as e, type SideEffectSemanticFieldStableCoreKey as f, type SdkDiagnosticCode as g, type SessionId as h, isSideEffectSemanticFieldKey as i, type GlasstraceOptions as j, type SdkInitResponse as k, type SdkHealthReport as l, type SourceMapUploadResponse as m, type SourceMapManifestResponse as n };
@@ -227,19 +227,54 @@ declare const SIDE_EFFECT_OPERATION_KINDS: readonly ["email", "calendar_link", "
227
227
  */
228
228
  type SideEffectOperationKind = (typeof SIDE_EFFECT_OPERATION_KINDS)[number];
229
229
  /**
230
- * Allowlisted semantic field keys for `recordSideEffect()` evidence.
230
+ * Stable-core semantic field keys for `recordSideEffect()` evidence.
231
231
  *
232
- * Only fields named here may be attached to a side-effect operation
233
- * summary. Any other key is silently dropped at the SDK boundary and
234
- * counted under the `unsupported_key` omission reason.
232
+ * These seven keys have specialized value validators (BCP-47 for
233
+ * `locale`, IANA for `timezone`, compact token for the rest) and are
234
+ * the closed set of names that can never be removed without a
235
+ * coordinated wire-breaking change. Pattern-admitted keys (see
236
+ * {@link SIDE_EFFECT_SEMANTIC_FIELD_OPEN_PATTERN}) live alongside
237
+ * these and are validated by suffix routing.
235
238
  */
236
- declare const SIDE_EFFECT_SEMANTIC_FIELD_KEYS: readonly ["templateKey", "providerOperation", "role", "locale", "timezone", "status", "phase", "recipientClass", "participantCount", "activeParticipantCount"];
239
+ declare const SIDE_EFFECT_SEMANTIC_FIELD_STABLE_CORE_KEYS: readonly ["templateKey", "providerOperation", "role", "locale", "timezone", "status", "phase"];
237
240
  /**
238
- * One of the allowlisted side-effect semantic field keys.
241
+ * One of the stable-core semantic field keys (compile-time literal
242
+ * union). Consumers who want autocomplete for the closed stable-core
243
+ * set import this narrower type; consumers who accept the wider
244
+ * named-pattern admission shape use {@link SideEffectSemanticFieldKey}.
239
245
  *
240
- * @see {@link SIDE_EFFECT_SEMANTIC_FIELD_KEYS}
246
+ * @see {@link SIDE_EFFECT_SEMANTIC_FIELD_STABLE_CORE_KEYS}
241
247
  */
242
- type SideEffectSemanticFieldKey = (typeof SIDE_EFFECT_SEMANTIC_FIELD_KEYS)[number];
248
+ type SideEffectSemanticFieldStableCoreKey = (typeof SIDE_EFFECT_SEMANTIC_FIELD_STABLE_CORE_KEYS)[number];
249
+ /**
250
+ * Runtime guard for the semantic field key admission contract.
251
+ *
252
+ * Returns `true` when `key` is non-empty, no longer than
253
+ * {@link MAX_SIDE_EFFECT_SEMANTIC_FIELD_KEY_LENGTH}, and either one
254
+ * of the seven stable-core keys or matches the open-pattern regex.
255
+ * Use this guard at any runtime call-site that needs to validate a
256
+ * producer-supplied key before emission; for compile-time
257
+ * autocomplete on the stable-core subset, import
258
+ * {@link SideEffectSemanticFieldStableCoreKey}.
259
+ *
260
+ * Note: the length cap is part of the admission contract, not a
261
+ * separate SDK runtime concern. Consumers using this guard see the
262
+ * same decision the SDK uses at `recordSideEffect()` emission.
263
+ */
264
+ declare function isSideEffectSemanticFieldKey(key: string): boolean;
265
+ /**
266
+ * Any admissible semantic field key — the stable-core literal union
267
+ * plus any string matching the open-pattern regex.
268
+ *
269
+ * Note: TypeScript collapses `<literal-union> | string` to `string`
270
+ * at the type level (the `string` member subsumes the literal arm),
271
+ * so compile-time narrowing is intentionally relaxed at this
272
+ * surface. Runtime admission is enforced by
273
+ * {@link isSideEffectSemanticFieldKey}. Consumers who want
274
+ * compile-time stable-core autocomplete should import
275
+ * {@link SideEffectSemanticFieldStableCoreKey} instead.
276
+ */
277
+ type SideEffectSemanticFieldKey = SideEffectSemanticFieldStableCoreKey | string;
243
278
  /**
244
279
  * Allowlisted reasons a side-effect value may be omitted from the
245
280
  * emitted summary.
@@ -281,4 +316,4 @@ declare const SIDE_EFFECT_OPERATION_PHASES: readonly ["request", "post_response"
281
316
  */
282
317
  type SideEffectOperationPhase = (typeof SIDE_EFFECT_OPERATION_PHASES)[number];
283
318
 
284
- export { type AnonApiKey as A, type CaptureConfig as C, type GlasstraceEnvVars as G, type ImportGraphPayload as I, type SideEffectOperationKind as S, type SideEffectOperationStatus as a, type SideEffectOperationPhase as b, type SideEffectSemanticFieldKey as c, deriveSessionId as d, type SideEffectOmissionReason as e, type SdkDiagnosticCode as f, type SessionId as g, type GlasstraceOptions as h, type SdkInitResponse as i, type SdkHealthReport as j, type SourceMapUploadResponse as k, type SourceMapManifestResponse as l };
319
+ export { type AnonApiKey as A, type CaptureConfig as C, type GlasstraceEnvVars as G, type ImportGraphPayload as I, type SideEffectOperationKind as S, type SideEffectOperationStatus as a, type SideEffectOperationPhase as b, type SideEffectSemanticFieldKey as c, deriveSessionId as d, type SideEffectOmissionReason as e, type SideEffectSemanticFieldStableCoreKey as f, type SdkDiagnosticCode as g, type SessionId as h, isSideEffectSemanticFieldKey as i, type GlasstraceOptions as j, type SdkInitResponse as k, type SdkHealthReport as l, type SourceMapUploadResponse as m, type SourceMapManifestResponse as n };
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- export { C as CorrelationIdRequest, G as GlasstraceSpanProcessor, S as SdkError, a as SessionManager, c as captureCorrelationId, g as getDateString, b as getOrigin } from './correlation-id-CFpyJsKv.cjs';
2
- export { F as FetchTarget, G as GlasstraceExporter, a as GlasstraceExporterOptions, I as InitClaimResult, R as ResolvedConfig, c as captureError, b as classifyFetchTarget, d as createGlasstraceSpanProcessor, g as getActiveConfig, e as getDiscoveryHandler, f as getLinkedAccountId, h as getOrCreateAnonKey, i as getStatus, j as isAnonymousMode, k as isProductionDisabled, l as isReady, m as loadCachedConfig, p as performInit, r as readAnonKey, n as readEnvVars, o as registerGlasstrace, q as resolveConfig, s as saveCachedConfig, t as sendInitRequest, w as waitForReady, u as withGlasstraceConfig } from './capture-error-CRQSqKgd.cjs';
3
- import { S as SideEffectOperationKind, a as SideEffectOperationStatus, b as SideEffectOperationPhase, c as SideEffectSemanticFieldKey } from './index.d-D-jdiIPU.cjs';
4
- export { e as SideEffectOmissionReason, d as deriveSessionId } from './index.d-D-jdiIPU.cjs';
1
+ export { C as CorrelationIdRequest, G as GlasstraceSpanProcessor, S as SdkError, a as SessionManager, c as captureCorrelationId, g as getDateString, b as getOrigin } from './correlation-id-CZ2bstzA.cjs';
2
+ export { F as FetchTarget, G as GlasstraceExporter, a as GlasstraceExporterOptions, I as InitClaimResult, R as ResolvedConfig, c as captureError, b as classifyFetchTarget, d as createGlasstraceSpanProcessor, g as getActiveConfig, e as getDiscoveryHandler, f as getLinkedAccountId, h as getOrCreateAnonKey, i as getStatus, j as isAnonymousMode, k as isProductionDisabled, l as isReady, m as loadCachedConfig, p as performInit, r as readAnonKey, n as readEnvVars, o as registerGlasstrace, q as resolveConfig, s as saveCachedConfig, t as sendInitRequest, w as waitForReady, u as withGlasstraceConfig } from './capture-error-03qDnC5v.cjs';
3
+ import { S as SideEffectOperationKind, a as SideEffectOperationStatus, b as SideEffectOperationPhase, c as SideEffectSemanticFieldKey } from './index.d-BQIJ5Dvc.cjs';
4
+ export { e as SideEffectOmissionReason, f as SideEffectSemanticFieldStableCoreKey, d as deriveSessionId, i as isSideEffectSemanticFieldKey } from './index.d-BQIJ5Dvc.cjs';
5
5
  export { RequestMiddlewareFunction, TracedRequestMiddlewareOptions } from './middleware/index.cjs';
6
6
  export { WithAsyncCausalityOptions } from './async-context/index.cjs';
7
7
  import './export/ReadableSpan';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { C as CorrelationIdRequest, G as GlasstraceSpanProcessor, S as SdkError, a as SessionManager, c as captureCorrelationId, g as getDateString, b as getOrigin } from './correlation-id-DPXyY9-3.js';
2
- export { F as FetchTarget, G as GlasstraceExporter, a as GlasstraceExporterOptions, I as InitClaimResult, R as ResolvedConfig, c as captureError, b as classifyFetchTarget, d as createGlasstraceSpanProcessor, g as getActiveConfig, e as getDiscoveryHandler, f as getLinkedAccountId, h as getOrCreateAnonKey, i as getStatus, j as isAnonymousMode, k as isProductionDisabled, l as isReady, m as loadCachedConfig, p as performInit, r as readAnonKey, n as readEnvVars, o as registerGlasstrace, q as resolveConfig, s as saveCachedConfig, t as sendInitRequest, w as waitForReady, u as withGlasstraceConfig } from './capture-error-CqCbjoMq.js';
3
- import { S as SideEffectOperationKind, a as SideEffectOperationStatus, b as SideEffectOperationPhase, c as SideEffectSemanticFieldKey } from './index.d-D-jdiIPU.js';
4
- export { e as SideEffectOmissionReason, d as deriveSessionId } from './index.d-D-jdiIPU.js';
1
+ export { C as CorrelationIdRequest, G as GlasstraceSpanProcessor, S as SdkError, a as SessionManager, c as captureCorrelationId, g as getDateString, b as getOrigin } from './correlation-id-YcfcqOru.js';
2
+ export { F as FetchTarget, G as GlasstraceExporter, a as GlasstraceExporterOptions, I as InitClaimResult, R as ResolvedConfig, c as captureError, b as classifyFetchTarget, d as createGlasstraceSpanProcessor, g as getActiveConfig, e as getDiscoveryHandler, f as getLinkedAccountId, h as getOrCreateAnonKey, i as getStatus, j as isAnonymousMode, k as isProductionDisabled, l as isReady, m as loadCachedConfig, p as performInit, r as readAnonKey, n as readEnvVars, o as registerGlasstrace, q as resolveConfig, s as saveCachedConfig, t as sendInitRequest, w as waitForReady, u as withGlasstraceConfig } from './capture-error-CAfFUyIU.js';
3
+ import { S as SideEffectOperationKind, a as SideEffectOperationStatus, b as SideEffectOperationPhase, c as SideEffectSemanticFieldKey } from './index.d-BQIJ5Dvc.js';
4
+ export { e as SideEffectOmissionReason, f as SideEffectSemanticFieldStableCoreKey, d as deriveSessionId, i as isSideEffectSemanticFieldKey } from './index.d-BQIJ5Dvc.js';
5
5
  export { RequestMiddlewareFunction, TracedRequestMiddlewareOptions } from './middleware/index.js';
6
6
  export { WithAsyncCausalityOptions } from './async-context/index.js';
7
7
  import './export/ReadableSpan';
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  getOrigin,
10
10
  registerGlasstrace,
11
11
  withGlasstraceConfig
12
- } from "./chunk-CZQN6G4I.js";
12
+ } from "./chunk-VWZMG3W2.js";
13
13
  import {
14
14
  getStatus,
15
15
  isReady,
@@ -19,7 +19,7 @@ import {
19
19
  GlasstraceSpanProcessor,
20
20
  SdkError,
21
21
  captureCorrelationId
22
- } from "./chunk-M3ZTG4P5.js";
22
+ } from "./chunk-HMEHYSTS.js";
23
23
  import "./chunk-CL3OVHPO.js";
24
24
  import {
25
25
  trace
@@ -32,7 +32,7 @@ import {
32
32
  performInit,
33
33
  saveCachedConfig,
34
34
  sendInitRequest
35
- } from "./chunk-6NKMAKMI.js";
35
+ } from "./chunk-6ST4QV7T.js";
36
36
  import {
37
37
  isAnonymousMode,
38
38
  isProductionDisabled,
@@ -42,29 +42,27 @@ import {
42
42
  import {
43
43
  getOrCreateAnonKey,
44
44
  readAnonKey
45
- } from "./chunk-J3ZCVE67.js";
45
+ } from "./chunk-F7A3QXCT.js";
46
46
  import {
47
47
  GLASSTRACE_ATTRIBUTE_NAMES,
48
48
  SIDE_EFFECT_OMISSION_REASONS,
49
49
  SIDE_EFFECT_OPERATION_KINDS,
50
50
  SIDE_EFFECT_OPERATION_PHASES,
51
51
  SIDE_EFFECT_OPERATION_STATUSES,
52
- SIDE_EFFECT_SEMANTIC_FIELD_KEYS,
53
- deriveSessionId
54
- } from "./chunk-BN5DVIUO.js";
52
+ deriveSessionId,
53
+ isSideEffectSemanticFieldKey
54
+ } from "./chunk-7LE2O4ZJ.js";
55
55
  import "./chunk-YIEXKQYP.js";
56
56
  import "./chunk-NSBPE2FW.js";
57
57
 
58
58
  // src/side-effect/allowlist.ts
59
59
  var MAX_SIDE_EFFECT_OPERATION_LABEL_LENGTH = 96;
60
60
  var MAX_SIDE_EFFECT_FIELD_VALUE_LENGTH = 80;
61
+ var MAX_SIDE_EFFECT_FIELD_COUNT_VALUE_LENGTH = 16;
61
62
  var MAX_SIDE_EFFECT_OPERATIONS_PER_SPAN = 5;
62
63
  var OPERATION_KIND_SET = new Set(
63
64
  SIDE_EFFECT_OPERATION_KINDS
64
65
  );
65
- var SEMANTIC_FIELD_KEY_SET = new Set(
66
- SIDE_EFFECT_SEMANTIC_FIELD_KEYS
67
- );
68
66
  var OPERATION_STATUS_SET = new Set(
69
67
  SIDE_EFFECT_OPERATION_STATUSES
70
68
  );
@@ -109,7 +107,7 @@ function detectUnsafePattern(value) {
109
107
  function passesFieldValidator(key, value) {
110
108
  if (key === "locale") return LOCALE_REGEX.test(value);
111
109
  if (key === "timezone") return TIMEZONE_REGEX.test(value);
112
- if (key === "participantCount" || key === "activeParticipantCount") {
110
+ if (typeof key === "string" && key.endsWith("Count")) {
113
111
  return DIGIT_REGEX.test(value);
114
112
  }
115
113
  return TOKEN_REGEX.test(value);
@@ -134,7 +132,8 @@ function checkSemanticFieldValue(key, value) {
134
132
  if (typeof value !== "string" || value.length === 0) {
135
133
  return { accepted: false, reason: "raw_payload" };
136
134
  }
137
- if (value.length > MAX_SIDE_EFFECT_FIELD_VALUE_LENGTH) {
135
+ const maxLength = typeof key === "string" && key.endsWith("Count") ? MAX_SIDE_EFFECT_FIELD_COUNT_VALUE_LENGTH : MAX_SIDE_EFFECT_FIELD_VALUE_LENGTH;
136
+ if (value.length > maxLength) {
138
137
  return { accepted: false, reason: "value_too_long" };
139
138
  }
140
139
  const unsafe = detectUnsafePattern(value);
@@ -147,7 +146,7 @@ function checkSemanticFieldValue(key, value) {
147
146
  return { accepted: true, value };
148
147
  }
149
148
  function checkSemanticFieldKey(key) {
150
- return typeof key === "string" && SEMANTIC_FIELD_KEY_SET.has(key);
149
+ return typeof key === "string" && isSideEffectSemanticFieldKey(key);
151
150
  }
152
151
  function checkOperationKind(kind) {
153
152
  return typeof kind === "string" && OPERATION_KIND_SET.has(kind);
@@ -223,6 +222,11 @@ var FIELD_ATTRIBUTE_BY_KEY = {
223
222
  participantCount: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_PARTICIPANT_COUNT,
224
223
  activeParticipantCount: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_ACTIVE_PARTICIPANT_COUNT
225
224
  };
225
+ function resolveFieldAttribute(key) {
226
+ const explicit = FIELD_ATTRIBUTE_BY_KEY[key];
227
+ if (explicit !== void 0) return explicit;
228
+ return `glasstrace.side_effect.field.${key}`;
229
+ }
226
230
  function attachOperation(input) {
227
231
  const span = getRecordingActiveSpan();
228
232
  if (!span) return { kind: "no_active_span" };
@@ -254,7 +258,7 @@ function attachOperation(input) {
254
258
  return { kind: "attached", span };
255
259
  }
256
260
  function attachField(span, key, value) {
257
- const attribute = FIELD_ATTRIBUTE_BY_KEY[key];
261
+ const attribute = resolveFieldAttribute(key);
258
262
  try {
259
263
  span.setAttribute(attribute, value);
260
264
  } catch {
@@ -356,6 +360,7 @@ export {
356
360
  isAnonymousMode,
357
361
  isProductionDisabled,
358
362
  isReady,
363
+ isSideEffectSemanticFieldKey,
359
364
  loadCachedConfig,
360
365
  performInit,
361
366
  readAnonKey,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/side-effect/allowlist.ts","../src/side-effect/emit.ts","../src/side-effect/index.ts"],"sourcesContent":["/**\n * Side-effect evidence allowlist enforcement (SDK-049).\n *\n * Two enforcement layers, both pure functions with no I/O. The SDK\n * runs these before any `glasstrace.side_effect.*` attribute is\n * attached to a span; the product's storage filter (ING-023) is a\n * second defense, not the primary boundary. A value rejected here\n * never reaches the OTel exporter, so it cannot leak through any\n * downstream telemetry path.\n *\n * Layer 1 (input shape):\n * - Reject non-string scalars where strings are required.\n * - Reject lengths exceeding the SCHEMA-036 budgets\n * (operation label > 96 chars, field value > 80 chars) ⇒\n * `value_too_long`.\n * - Reject unsafe-pattern matches (URL, email, headers, tokens,\n * UUIDs, prose-shaped whitespace) ⇒ category-mapped reason.\n *\n * Layer 2 (per-field validators):\n * - `templateKey | providerOperation | role | status | phase` and\n * the operation label require a compact token regex.\n * - `locale` requires a BCP-47-shaped token.\n * - `timezone` requires an IANA-shaped token.\n *\n * The regexes mirror the product-side schema in\n * `glasstrace-product/shared/types/agent-evidence.ts:585-619`\n * verbatim so any value the SDK admits is guaranteed to pass the\n * product's storage-time filter.\n *\n * @drift-check ../../glasstrace-product/shared/types/agent-evidence.ts\n */\n\nimport type {\n SideEffectOmissionReason,\n SideEffectOperationKind,\n SideEffectOperationPhase,\n SideEffectOperationStatus,\n SideEffectSemanticFieldKey,\n} from \"@glasstrace/protocol\";\nimport {\n SIDE_EFFECT_OMISSION_REASONS,\n SIDE_EFFECT_OPERATION_KINDS,\n SIDE_EFFECT_OPERATION_PHASES,\n SIDE_EFFECT_OPERATION_STATUSES,\n SIDE_EFFECT_SEMANTIC_FIELD_KEYS,\n} from \"@glasstrace/protocol\";\n\n/**\n * Maximum length, in characters, of a side-effect operation label.\n * Mirrors `AGENT_EVIDENCE_MAX_SIDE_EFFECT_OPERATION_LABEL_LENGTH`\n * in the product-side schema.\n */\nexport const MAX_SIDE_EFFECT_OPERATION_LABEL_LENGTH = 96;\n\n/**\n * Maximum length, in characters, of a side-effect semantic field\n * value. Mirrors `AGENT_EVIDENCE_MAX_SIDE_EFFECT_FIELD_VALUE_LENGTH`.\n */\nexport const MAX_SIDE_EFFECT_FIELD_VALUE_LENGTH = 80;\n\n/**\n * Maximum number of side-effect operations recorded on a single span\n * before further calls are dropped under the `value_too_long`\n * omission reason. Mirrors\n * `AGENT_EVIDENCE_MAX_SIDE_EFFECT_OPERATIONS`.\n */\nexport const MAX_SIDE_EFFECT_OPERATIONS_PER_SPAN = 5;\n\nconst OPERATION_KIND_SET: ReadonlySet<string> = new Set(\n SIDE_EFFECT_OPERATION_KINDS,\n);\nconst SEMANTIC_FIELD_KEY_SET: ReadonlySet<string> = new Set(\n SIDE_EFFECT_SEMANTIC_FIELD_KEYS,\n);\nconst OPERATION_STATUS_SET: ReadonlySet<string> = new Set(\n SIDE_EFFECT_OPERATION_STATUSES,\n);\nconst OPERATION_PHASE_SET: ReadonlySet<string> = new Set(\n SIDE_EFFECT_OPERATION_PHASES,\n);\nconst OMISSION_REASON_SET: ReadonlySet<string> = new Set(\n SIDE_EFFECT_OMISSION_REASONS,\n);\n\n/**\n * Returns `true` when `reason` is one of the SCHEMA-036 omission\n * reasons. Exposed for tests and any future caller that needs to\n * narrow an arbitrary string to a `SideEffectOmissionReason` before\n * passing it to the emission helpers; the SDK's own emission path\n * always works with statically known literals.\n */\nexport function isKnownOmissionReason(\n reason: string,\n): reason is SideEffectOmissionReason {\n return OMISSION_REASON_SET.has(reason);\n}\n\n// Compact-token regex shared by templateKey/providerOperation/role/status/phase\n// and the operation label. The trailing length guard is enforced\n// separately so the budget rejection produces `value_too_long`\n// (rather than this regex's anchor failure being mis-categorized).\nconst TOKEN_REGEX = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/;\n\n// BCP-47-shaped locale, mirroring SCHEMA-036's\n// SideEffectLocaleValueSchema regex.\nconst LOCALE_REGEX = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}$/;\n\n// IANA-shaped timezone, mirroring SCHEMA-036's\n// SideEffectTimezoneValueSchema regex.\nconst TIMEZONE_REGEX =\n /^(?:UTC|GMT|[A-Za-z][A-Za-z0-9_+-]*(?:\\/[A-Za-z0-9_+-]+){1,3})$/;\n\n// Non-negative integer string for participant-count fields. Tighter\n// than TOKEN_REGEX so misleading non-digit values (`\"many\"`, `\"a few\"`,\n// `\"1:2\"`) are rejected as `raw_payload` rather than recorded as\n// causal evidence. The leading anchor reproduces the TOKEN_REGEX\n// rejection of signed counts (`\"-1\"`) and rejects empty strings.\nconst DIGIT_REGEX = /^[0-9]+$/;\n\n// Unsafe-pattern detectors. Each rejects independently; the first\n// match determines the omission reason. The patterns and the reason\n// mapping are calibrated against the product's\n// SideEffectUnsafeTextSchema (agent-evidence.ts:563-583) and the\n// SDK-049 §Safety Requirements list.\n\nconst URL_SCHEME = /:\\/\\//;\nconst URL_SCHEME_RELATIVE = /^\\/\\//;\nconst EMAIL_LIKE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i;\nconst QUERY_LIKE = /\\?/;\nconst FRAGMENT_LIKE = /#/;\nconst HEADER_LIKE = /\\b(authorization|set-cookie|cookie)\\b\\s*[:=]/i;\nconst HEADER_TOKEN_LIKE =\n /\\b(authorization|set-cookie|cookie)\\b\\s+\\S+=/i;\nconst BEARER_LIKE = /bearer\\s+\\S+/i;\nconst TOKEN_KV_LIKE =\n /[\"']?(password|passwd|token|api[_-]?key|secret|client_secret)[\"']?\\s*[:=]/i;\nconst PROSE_LIKE = /[\\r\\n\\t]|\\s{2,}/;\nconst UUID_LIKE =\n /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i;\nconst GT_KEY_LIKE = /gt_(dev|anon|live)_[A-Za-z0-9_-]+/;\n\n/**\n * Inspect a value for unsafe patterns. Returns the matched omission\n * reason or `null` if the value is shape-clean. The function does NOT\n * apply the per-field token regex; that is Layer 2 below.\n *\n * Detector order matters: URL-shape detectors run before token-shape\n * detectors so a URL with an embedded `token=` query parameter is\n * categorized as `raw_payload` (the structural shape that brought it\n * here was the URL, not the token). Header and bearer-shaped values\n * are categorized as `secret` because they always carry credential\n * material. Email is `pii`. Whitespace anomalies are `raw_payload`.\n */\nfunction detectUnsafePattern(value: string): SideEffectOmissionReason | null {\n // Whitespace anomalies fall into raw_payload because they indicate\n // prose or copy-pasted user content rather than a compact label.\n if (value.trim() !== value) return \"raw_payload\";\n if (PROSE_LIKE.test(value)) return \"raw_payload\";\n\n // URL-shape detectors run first so a credential-bearing query\n // string is categorized by its structural shape (raw_payload)\n // rather than the credential token inside it. The product's\n // SCHEMA-036 filter rejects URL shapes regardless of category, so\n // either choice is safe; the test fixture documents that URL-shape\n // wins.\n if (URL_SCHEME.test(value)) return \"raw_payload\";\n if (URL_SCHEME_RELATIVE.test(value)) return \"raw_payload\";\n if (QUERY_LIKE.test(value)) return \"raw_payload\";\n if (FRAGMENT_LIKE.test(value)) return \"raw_payload\";\n\n // Header-shaped and credential-shaped values route to `secret`\n // because they always carry authentication material. Bearer- and\n // token-key-value-like values are detected even when no header\n // prefix is present.\n if (BEARER_LIKE.test(value)) return \"secret\";\n if (HEADER_TOKEN_LIKE.test(value)) return \"secret\";\n if (TOKEN_KV_LIKE.test(value)) return \"secret\";\n if (HEADER_LIKE.test(value)) return \"secret\";\n if (UUID_LIKE.test(value)) return \"secret\";\n if (GT_KEY_LIKE.test(value)) return \"secret\";\n\n // PII detector.\n if (EMAIL_LIKE.test(value)) return \"pii\";\n\n return null;\n}\n\n/**\n * Run Layer 2 per-field validation. Returns `true` when the value\n * matches the field's regex. Length and shape rejection from Layer 1\n * is the caller's responsibility — by the time this runs the value\n * is known to be a non-empty string within the per-field length\n * budget and free of unsafe patterns.\n */\nfunction passesFieldValidator(\n key: SideEffectSemanticFieldKey,\n value: string,\n): boolean {\n if (key === \"locale\") return LOCALE_REGEX.test(value);\n if (key === \"timezone\") return TIMEZONE_REGEX.test(value);\n if (key === \"participantCount\" || key === \"activeParticipantCount\") {\n return DIGIT_REGEX.test(value);\n }\n return TOKEN_REGEX.test(value);\n}\n\n/**\n * Outcome of allowlist enforcement on a single value.\n */\nexport type ValueOutcome =\n | { accepted: true; value: string }\n | { accepted: false; reason: SideEffectOmissionReason };\n\n/**\n * Check the operation label (`recordSideEffect({ operation })`).\n *\n * Order matters: type rejection ⇒ length budget ⇒ unsafe pattern ⇒\n * field validator. This ordering lets each rejection class produce a\n * meaningful omission reason without the regex anchor failure\n * masking an upstream length or shape problem.\n */\nexport function checkOperationLabel(value: unknown): ValueOutcome {\n if (typeof value !== \"string\" || value.length === 0) {\n return { accepted: false, reason: \"raw_payload\" };\n }\n if (value.length > MAX_SIDE_EFFECT_OPERATION_LABEL_LENGTH) {\n return { accepted: false, reason: \"value_too_long\" };\n }\n const unsafe = detectUnsafePattern(value);\n if (unsafe) {\n return { accepted: false, reason: unsafe };\n }\n if (!TOKEN_REGEX.test(value)) {\n // A value that survives unsafe-pattern detection but still fails\n // the compact-token regex is malformed (e.g., starts with a hyphen\n // or contains a slash). Categorize as `raw_payload` because the\n // value carries shape inconsistent with a normalized label.\n return { accepted: false, reason: \"raw_payload\" };\n }\n return { accepted: true, value };\n}\n\n/**\n * Check a semantic field value for one of the allowlisted keys. The\n * key must already be known to be allowlisted (Layer 1 filters\n * unsupported keys via {@link checkSemanticFieldKey}).\n */\nexport function checkSemanticFieldValue(\n key: SideEffectSemanticFieldKey,\n value: unknown,\n): ValueOutcome {\n if (typeof value !== \"string\" || value.length === 0) {\n return { accepted: false, reason: \"raw_payload\" };\n }\n if (value.length > MAX_SIDE_EFFECT_FIELD_VALUE_LENGTH) {\n return { accepted: false, reason: \"value_too_long\" };\n }\n const unsafe = detectUnsafePattern(value);\n if (unsafe) {\n return { accepted: false, reason: unsafe };\n }\n if (!passesFieldValidator(key, value)) {\n return { accepted: false, reason: \"raw_payload\" };\n }\n return { accepted: true, value };\n}\n\n/**\n * Returns `true` when `key` is allowlisted as a semantic field key.\n * The narrowing predicate lets callers safely route the value to\n * {@link checkSemanticFieldValue}.\n */\nexport function checkSemanticFieldKey(\n key: unknown,\n): key is SideEffectSemanticFieldKey {\n return typeof key === \"string\" && SEMANTIC_FIELD_KEY_SET.has(key);\n}\n\n/**\n * Returns `true` when `kind` is one of the v1 allowlisted operation\n * kinds.\n */\nexport function checkOperationKind(\n kind: unknown,\n): kind is SideEffectOperationKind {\n return typeof kind === \"string\" && OPERATION_KIND_SET.has(kind);\n}\n\n/**\n * Returns `true` when `status` is one of the v1 allowlisted operation\n * statuses. Distinct from the per-field `status` check because the\n * top-level operation status is enforced as an enum membership only —\n * no compact-token regex applies (the SCHEMA-036 enum is the\n * exhaustive accepted set).\n */\nexport function checkOperationStatus(\n status: unknown,\n): status is SideEffectOperationStatus {\n return typeof status === \"string\" && OPERATION_STATUS_SET.has(status);\n}\n\n/**\n * Returns `true` when `phase` is one of the v1 allowlisted operation\n * phases.\n */\nexport function checkOperationPhase(\n phase: unknown,\n): phase is SideEffectOperationPhase {\n return typeof phase === \"string\" && OPERATION_PHASE_SET.has(phase);\n}\n","/**\n * Side-effect evidence attribute emission and per-span counter state\n * (SDK-049).\n *\n * Pure observer: this module never executes a side effect, never\n * creates a span, never throws to the caller. All failure modes route\n * to silent no-ops or omission counters. Per-span state is held in a\n * `WeakMap` keyed by the OTel `Span` object so counters auto-clean\n * when the span is garbage-collected after export.\n */\n\nimport * as otelApi from \"@opentelemetry/api\";\nimport {\n GLASSTRACE_ATTRIBUTE_NAMES,\n type SideEffectOmissionReason,\n type SideEffectOperationKind,\n type SideEffectOperationPhase,\n type SideEffectOperationStatus,\n type SideEffectSemanticFieldKey,\n} from \"@glasstrace/protocol\";\nimport { MAX_SIDE_EFFECT_OPERATIONS_PER_SPAN } from \"./allowlist.js\";\n\n/**\n * Per-span side-effect bookkeeping. `operationsRecorded` enforces the\n * SCHEMA-036 budget of 5 operations per span; `omissions` carries one\n * counter per omission reason so final `glasstrace.side_effect.omitted.*`\n * attributes can be flushed on the span as integer counts.\n */\ninterface SpanSideEffectState {\n operationsRecorded: number;\n omissions: Map<SideEffectOmissionReason, number>;\n}\n\nconst spanState: WeakMap<otelApi.Span, SpanSideEffectState> = new WeakMap();\n\nfunction getOrCreateState(span: otelApi.Span): SpanSideEffectState {\n let state = spanState.get(span);\n if (!state) {\n state = { operationsRecorded: 0, omissions: new Map() };\n spanState.set(span, state);\n }\n return state;\n}\n\n/**\n * Returns the currently-active span when it is recording and not yet\n * ended. Returns `undefined` when there is no active span, when the\n * active span is a `NonRecordingSpan`, or when the active span has\n * already ended. Callers treat all such cases as silent no-op\n * conditions: there is no span on which to attach evidence.\n */\nfunction getRecordingActiveSpan(): otelApi.Span | undefined {\n let span: otelApi.Span | undefined;\n try {\n span = otelApi.trace.getActiveSpan();\n } catch {\n // Defensive: an OTel API surface error must not propagate to the\n // user's side-effect call site.\n return undefined;\n }\n if (!span) return undefined;\n\n // `isRecording()` returns false for both NonRecordingSpan and ended\n // spans on the standard SDK; honor that as the no-op signal. The\n // method is part of the OTel API contract so a missing impl\n // indicates a host shim — fall through to the conservative no-op.\n try {\n if (typeof span.isRecording === \"function\" && !span.isRecording()) {\n return undefined;\n }\n } catch {\n return undefined;\n }\n return span;\n}\n\n/**\n * Record a single omission count on the active span without emitting\n * the rejected value. Rejection metadata only ever leaks the integer\n * count, never the original input.\n *\n * No-op when there is no recording active span — the rejected value\n * still doesn't reach the wire because emission is gated on a span\n * being available.\n */\nexport function recordOmissionOnActiveSpan(\n reason: SideEffectOmissionReason,\n): void {\n const span = getRecordingActiveSpan();\n if (!span) return;\n recordOmissionOnSpan(span, reason);\n}\n\nfunction recordOmissionOnSpan(\n span: otelApi.Span,\n reason: SideEffectOmissionReason,\n): void {\n const state = getOrCreateState(span);\n const previous = state.omissions.get(reason) ?? 0;\n const next = previous + 1;\n state.omissions.set(reason, next);\n\n const attribute = OMISSION_ATTRIBUTE_BY_REASON[reason];\n try {\n span.setAttribute(attribute, next);\n } catch {\n // OTel may reject the attribute write (slot exhaustion, ended\n // span). The counter still advances in-memory; further emission\n // attempts are harmless no-ops.\n }\n}\n\nconst OMISSION_ATTRIBUTE_BY_REASON: Readonly<\n Record<SideEffectOmissionReason, string>\n> = {\n pii: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_PII,\n secret: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_SECRET,\n raw_payload: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_RAW_PAYLOAD,\n unsupported_key:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_UNSUPPORTED_KEY,\n value_too_long:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_VALUE_TOO_LONG,\n not_emitted: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_NOT_EMITTED,\n capture_disabled:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_CAPTURE_DISABLED,\n};\n\nconst FIELD_ATTRIBUTE_BY_KEY: Readonly<\n Record<SideEffectSemanticFieldKey, string>\n> = {\n templateKey: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_TEMPLATE_KEY,\n providerOperation:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_PROVIDER_OPERATION,\n role: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_ROLE,\n locale: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_LOCALE,\n timezone: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_TIMEZONE,\n status: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_STATUS,\n phase: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_PHASE,\n recipientClass: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_RECIPIENT_CLASS,\n participantCount:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_PARTICIPANT_COUNT,\n activeParticipantCount:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_ACTIVE_PARTICIPANT_COUNT,\n};\n\n/**\n * Outcome of attempting to attach an operation summary. The\n * `over_budget` and `no_active_span` discriminants let the public API\n * route the call's bookkeeping (omission count vs. silent drop)\n * without re-querying span state.\n */\nexport type AttachOutcome =\n | { kind: \"attached\"; span: otelApi.Span }\n | { kind: \"no_active_span\" }\n | { kind: \"over_budget\"; span: otelApi.Span };\n\n/**\n * Attach the top-level operation attributes to the active span and\n * advance the per-span operation counter. The caller is responsible\n * for invoking {@link attachField} for each accepted semantic field\n * and {@link recordOmission} for each rejected value.\n *\n * Returns the span when emission proceeded, `no_active_span` when no\n * recording span is active, or `over_budget` when the per-span\n * operation budget (5) is exhausted. The caller routes `over_budget`\n * to a `value_too_long` omission via {@link recordOmission}.\n */\nexport function attachOperation(input: {\n kind: SideEffectOperationKind;\n operation: string;\n status?: SideEffectOperationStatus;\n phase?: SideEffectOperationPhase;\n}): AttachOutcome {\n const span = getRecordingActiveSpan();\n if (!span) return { kind: \"no_active_span\" };\n\n const state = getOrCreateState(span);\n if (state.operationsRecorded >= MAX_SIDE_EFFECT_OPERATIONS_PER_SPAN) {\n return { kind: \"over_budget\", span };\n }\n state.operationsRecorded += 1;\n\n try {\n span.setAttribute(GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_KIND, input.kind);\n span.setAttribute(\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OPERATION,\n input.operation,\n );\n if (input.status !== undefined) {\n span.setAttribute(\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_STATUS,\n input.status,\n );\n }\n if (input.phase !== undefined) {\n span.setAttribute(\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_PHASE,\n input.phase,\n );\n }\n } catch {\n // Slot exhaustion or ended-span write — ignore. The counter has\n // already advanced; subsequent calls within the same span are\n // budget-bounded as expected.\n }\n return { kind: \"attached\", span };\n}\n\n/**\n * Attach a single allowlisted semantic field to the span. The caller\n * has already routed the field key and value through the allowlist\n * helpers; this function only writes the attribute.\n */\nexport function attachField(\n span: otelApi.Span,\n key: SideEffectSemanticFieldKey,\n value: string,\n): void {\n const attribute = FIELD_ATTRIBUTE_BY_KEY[key];\n try {\n span.setAttribute(attribute, value);\n } catch {\n // Slot exhaustion — ignore.\n }\n}\n\n/**\n * Record an omission directly on a known span. Used by the public\n * API after {@link attachOperation} returns `over_budget` so the\n * count is registered on the same span that observed the operation.\n */\nexport function recordOmission(\n span: otelApi.Span,\n reason: SideEffectOmissionReason,\n): void {\n recordOmissionOnSpan(span, reason);\n}\n","/**\n * Public side-effect evidence emission API (SDK-049).\n *\n * Exposes {@link recordSideEffect} as a single user-callable function\n * that attaches allowlisted, non-sensitive semantic metadata about a\n * side-effect operation (email, calendar_link, webhook, external_api,\n * queue, after_callback) to the current active OTel span.\n *\n * The behavior contract is observational only: this function never\n * executes a side effect, never retries, never delays, never throws.\n * All failure modes (no active span, ended span, NonRecordingSpan,\n * capture-config disabled, allowlist rejection, per-span budget\n * exhausted, OTel attribute slot exhaustion) silently route to a\n * no-op or to an omission-counter increment that carries no rejected\n * input.\n */\n\nimport type {\n SideEffectOperationKind,\n SideEffectOperationPhase,\n SideEffectOperationStatus,\n SideEffectSemanticFieldKey,\n} from \"@glasstrace/protocol\";\nimport {\n checkOperationKind,\n checkOperationLabel,\n checkOperationPhase,\n checkOperationStatus,\n checkSemanticFieldKey,\n checkSemanticFieldValue,\n} from \"./allowlist.js\";\nimport {\n attachField,\n attachOperation,\n recordOmission,\n recordOmissionOnActiveSpan,\n} from \"./emit.js\";\nimport { getActiveConfig } from \"../init-client.js\";\n\n/**\n * Input shape for {@link recordSideEffect}.\n *\n * All fields except `kind` and `operation` are optional. The SDK\n * silently drops unknown fields and unsafe values, surfacing only an\n * integer omission count under the matching\n * `glasstrace.side_effect.omitted.*` attribute on the active span.\n */\nexport interface RecordSideEffectInput {\n /**\n * One of the allowlisted v1 operation kinds. Calls with any other\n * value (typo, unsupported kind, non-string) silently drop without\n * recording an omission, because there is no kind to attach the\n * counter to.\n */\n kind: SideEffectOperationKind;\n\n /**\n * Compact, normalized operation label (max 96 chars). Must match\n * `^[A-Za-z0-9][A-Za-z0-9_.:-]*$`. Free-form prose, URLs, query\n * strings, and email-shaped values are silently dropped and routed\n * to the matching omission counter.\n */\n operation: string;\n\n /**\n * Optional operation lifecycle status. Defaults to omitted. Values\n * outside the v1 allowlist are silently dropped.\n */\n status?: SideEffectOperationStatus;\n\n /**\n * Optional operation execution phase (request / post_response /\n * background / unknown). Defaults to omitted.\n */\n phase?: SideEffectOperationPhase;\n\n /**\n * Optional allowlisted semantic fields. Keys outside the v1\n * allowlist (`templateKey`, `providerOperation`, `role`, `locale`,\n * `timezone`, `status`, `phase`) and values matching unsafe\n * patterns (URLs, emails, tokens, headers, prose-shaped\n * whitespace) are silently dropped and routed to the matching\n * omission counter.\n */\n fields?: Partial<Record<SideEffectSemanticFieldKey, string>>;\n}\n\n/**\n * Record allowlisted side-effect evidence on the current active OTel\n * span (SDK-049).\n *\n * Behavior is observational only: this function never executes,\n * retries, or duplicates a side effect. The default capture-config\n * flag `sideEffectEvidence` is `false`; callers must opt in via\n * account configuration before any attribute reaches the wire.\n *\n * Edge cases (all silent no-ops):\n * - capture-config flag is `false` ⇒ no-op (no allowlist evaluation)\n * - input is not a plain object ⇒ no-op\n * - `kind` is not in the v1 allowlist ⇒ no-op\n * - no active span ⇒ no-op\n * - active span has already ended or is `NonRecordingSpan` ⇒ no-op\n * - per-span operation budget exhausted (5 ops max) ⇒ records a\n * `value_too_long` omission count, no operation attributes\n * - OTel attribute slot exhaustion ⇒ silently drops the attribute\n * write\n *\n * The SDK guards only callers of this function. Direct\n * `span.setAttribute(\"glasstrace.side_effect.<...>\", ...)` writes\n * bypass the SDK and rely on the product's storage filter (ING-023)\n * as the second defense layer; this is intentional defense-in-depth,\n * not a gap.\n *\n * @example Recording a successful cancellation email\n * ```ts\n * import { recordSideEffect } from \"@glasstrace/sdk\";\n *\n * await mailer.send({ to: recipient, template: \"EventCanceledEmail\" });\n * recordSideEffect({\n * kind: \"email\",\n * operation: \"email.send\",\n * status: \"succeeded\",\n * phase: \"request\",\n * fields: {\n * templateKey: \"EventCanceledEmail\",\n * role: \"invitee\",\n * locale: \"en-US\",\n * timezone: \"Europe/Paris\",\n * },\n * });\n * ```\n */\nexport function recordSideEffect(input: RecordSideEffectInput): void {\n try {\n runRecordSideEffect(input);\n } catch {\n // Defense-in-depth: any unexpected throw inside the function\n // (e.g., a host shim mis-implementing OTel API) must not\n // propagate to the user's code path. Behavior-neutrality requires\n // recordSideEffect to be observationally invisible.\n }\n}\n\nfunction runRecordSideEffect(input: unknown): void {\n if (!input || typeof input !== \"object\") return;\n\n // Capture-config gate: read at every call so config rotation takes\n // effect on the next emission without restart. The disk read is\n // cached inside getActiveConfig() so this stays cheap on the hot\n // path.\n let captureEnabled: boolean;\n try {\n captureEnabled = getActiveConfig().sideEffectEvidence === true;\n } catch {\n captureEnabled = false;\n }\n if (!captureEnabled) {\n // Note: we deliberately do NOT increment a `capture_disabled`\n // omission counter for every call. With the flag off, the SDK's\n // contract is \"no allowlist evaluation runs and no allocation\n // happens\" — surfacing a per-call counter would require attaching\n // to a span and would defeat that goal. The\n // `capture_disabled` reason exists for the receiver-side path\n // where ingestion drops attributes due to product-side flag\n // changes after the SDK emitted them.\n return;\n }\n\n const candidate = input as Partial<RecordSideEffectInput>;\n\n if (!checkOperationKind(candidate.kind)) {\n // No `kind` to attach a counter under — silent drop.\n return;\n }\n\n const labelOutcome = checkOperationLabel(candidate.operation);\n if (!labelOutcome.accepted) {\n recordOmissionOnActiveSpan(labelOutcome.reason);\n return;\n }\n\n let acceptedStatus: SideEffectOperationStatus | undefined;\n if (candidate.status !== undefined) {\n if (checkOperationStatus(candidate.status)) {\n acceptedStatus = candidate.status;\n } else {\n recordOmissionOnActiveSpan(\"unsupported_key\");\n }\n }\n\n let acceptedPhase: SideEffectOperationPhase | undefined;\n if (candidate.phase !== undefined) {\n if (checkOperationPhase(candidate.phase)) {\n acceptedPhase = candidate.phase;\n } else {\n recordOmissionOnActiveSpan(\"unsupported_key\");\n }\n }\n\n const outcome = attachOperation({\n kind: candidate.kind,\n operation: labelOutcome.value,\n status: acceptedStatus,\n phase: acceptedPhase,\n });\n\n if (outcome.kind === \"no_active_span\") {\n // No span to record an omission against either — silent drop.\n return;\n }\n if (outcome.kind === \"over_budget\") {\n recordOmission(outcome.span, \"value_too_long\");\n return;\n }\n\n // Process semantic fields. Each rejection routes to an omission\n // count on the same span; accepted values become field attributes.\n const fields = candidate.fields;\n if (fields && typeof fields === \"object\") {\n for (const [rawKey, rawValue] of Object.entries(fields)) {\n if (!checkSemanticFieldKey(rawKey)) {\n recordOmission(outcome.span, \"unsupported_key\");\n continue;\n }\n const valueOutcome = checkSemanticFieldValue(rawKey, rawValue);\n if (!valueOutcome.accepted) {\n recordOmission(outcome.span, valueOutcome.reason);\n continue;\n }\n attachField(outcome.span, rawKey, valueOutcome.value);\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/side-effect/allowlist.ts","../src/side-effect/emit.ts","../src/side-effect/index.ts"],"sourcesContent":["/**\n * Side-effect evidence allowlist enforcement (SDK-049).\n *\n * Two enforcement layers, both pure functions with no I/O. The SDK\n * runs these before any `glasstrace.side_effect.*` attribute is\n * attached to a span; the product's storage filter (ING-023) is a\n * second defense, not the primary boundary. A value rejected here\n * never reaches the OTel exporter, so it cannot leak through any\n * downstream telemetry path.\n *\n * Layer 1 (input shape):\n * - Reject non-string scalars where strings are required.\n * - Reject lengths exceeding the SCHEMA-036 budgets\n * (operation label > 96 chars, field value > 80 chars) ⇒\n * `value_too_long`.\n * - Reject unsafe-pattern matches (URL, email, headers, tokens,\n * UUIDs, prose-shaped whitespace) ⇒ category-mapped reason.\n *\n * Layer 2 (per-field validators):\n * - `templateKey | providerOperation | role | status | phase` and\n * the operation label require a compact token regex.\n * - `locale` requires a BCP-47-shaped token.\n * - `timezone` requires an IANA-shaped token.\n *\n * The regexes mirror the product-side schema in\n * `glasstrace-product/shared/types/agent-evidence.ts:585-619`\n * verbatim so any value the SDK admits is guaranteed to pass the\n * product's storage-time filter.\n *\n * @drift-check ../../glasstrace-product/shared/types/agent-evidence.ts\n */\n\nimport type {\n SideEffectOmissionReason,\n SideEffectOperationKind,\n SideEffectOperationPhase,\n SideEffectOperationStatus,\n SideEffectSemanticFieldKey,\n} from \"@glasstrace/protocol\";\nimport {\n isSideEffectSemanticFieldKey,\n SIDE_EFFECT_OMISSION_REASONS,\n SIDE_EFFECT_OPERATION_KINDS,\n SIDE_EFFECT_OPERATION_PHASES,\n SIDE_EFFECT_OPERATION_STATUSES,\n} from \"@glasstrace/protocol\";\n\n/**\n * Maximum length, in characters, of a side-effect operation label.\n * Mirrors `AGENT_EVIDENCE_MAX_SIDE_EFFECT_OPERATION_LABEL_LENGTH`\n * in the product-side schema.\n */\nexport const MAX_SIDE_EFFECT_OPERATION_LABEL_LENGTH = 96;\n\n/**\n * Maximum length, in characters, of a side-effect semantic field\n * value for stable-core keys (other than `locale` / `timezone`,\n * which use specialized validators) and for pattern-admitted\n * `*Class` / `*Kind` / `*Role` keys. Mirrors\n * `AGENT_EVIDENCE_MAX_SIDE_EFFECT_FIELD_VALUE_LENGTH`.\n */\nexport const MAX_SIDE_EFFECT_FIELD_VALUE_LENGTH = 80;\n\n/**\n * Maximum length, in characters, of a pattern-admitted `*Count`\n * value. Tighter than the default field-value length because count\n * values are non-negative integer strings, not free-form tokens.\n * Mirrors `AGENT_EVIDENCE_MAX_SIDE_EFFECT_FIELD_COUNT_VALUE_LENGTH`.\n */\nexport const MAX_SIDE_EFFECT_FIELD_COUNT_VALUE_LENGTH = 16;\n\n\n/**\n * Maximum number of side-effect operations recorded on a single span\n * before further calls are dropped under the `value_too_long`\n * omission reason. Mirrors\n * `AGENT_EVIDENCE_MAX_SIDE_EFFECT_OPERATIONS`.\n */\nexport const MAX_SIDE_EFFECT_OPERATIONS_PER_SPAN = 5;\n\nconst OPERATION_KIND_SET: ReadonlySet<string> = new Set(\n SIDE_EFFECT_OPERATION_KINDS,\n);\nconst OPERATION_STATUS_SET: ReadonlySet<string> = new Set(\n SIDE_EFFECT_OPERATION_STATUSES,\n);\nconst OPERATION_PHASE_SET: ReadonlySet<string> = new Set(\n SIDE_EFFECT_OPERATION_PHASES,\n);\nconst OMISSION_REASON_SET: ReadonlySet<string> = new Set(\n SIDE_EFFECT_OMISSION_REASONS,\n);\n\n/**\n * Returns `true` when `reason` is one of the SCHEMA-036 omission\n * reasons. Exposed for tests and any future caller that needs to\n * narrow an arbitrary string to a `SideEffectOmissionReason` before\n * passing it to the emission helpers; the SDK's own emission path\n * always works with statically known literals.\n */\nexport function isKnownOmissionReason(\n reason: string,\n): reason is SideEffectOmissionReason {\n return OMISSION_REASON_SET.has(reason);\n}\n\n// Compact-token regex shared by templateKey/providerOperation/role/status/phase\n// and the operation label. The trailing length guard is enforced\n// separately so the budget rejection produces `value_too_long`\n// (rather than this regex's anchor failure being mis-categorized).\nconst TOKEN_REGEX = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/;\n\n// BCP-47-shaped locale, mirroring SCHEMA-036's\n// SideEffectLocaleValueSchema regex.\nconst LOCALE_REGEX = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}$/;\n\n// IANA-shaped timezone, mirroring SCHEMA-036's\n// SideEffectTimezoneValueSchema regex.\nconst TIMEZONE_REGEX =\n /^(?:UTC|GMT|[A-Za-z][A-Za-z0-9_+-]*(?:\\/[A-Za-z0-9_+-]+){1,3})$/;\n\n// Non-negative integer string for participant-count fields. Tighter\n// than TOKEN_REGEX so misleading non-digit values (`\"many\"`, `\"a few\"`,\n// `\"1:2\"`) are rejected as `raw_payload` rather than recorded as\n// causal evidence. The leading anchor reproduces the TOKEN_REGEX\n// rejection of signed counts (`\"-1\"`) and rejects empty strings.\nconst DIGIT_REGEX = /^[0-9]+$/;\n\n// Unsafe-pattern detectors. Each rejects independently; the first\n// match determines the omission reason. The patterns and the reason\n// mapping are calibrated against the product's\n// SideEffectUnsafeTextSchema (agent-evidence.ts:563-583) and the\n// SDK-049 §Safety Requirements list.\n\nconst URL_SCHEME = /:\\/\\//;\nconst URL_SCHEME_RELATIVE = /^\\/\\//;\nconst EMAIL_LIKE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i;\nconst QUERY_LIKE = /\\?/;\nconst FRAGMENT_LIKE = /#/;\nconst HEADER_LIKE = /\\b(authorization|set-cookie|cookie)\\b\\s*[:=]/i;\nconst HEADER_TOKEN_LIKE =\n /\\b(authorization|set-cookie|cookie)\\b\\s+\\S+=/i;\nconst BEARER_LIKE = /bearer\\s+\\S+/i;\nconst TOKEN_KV_LIKE =\n /[\"']?(password|passwd|token|api[_-]?key|secret|client_secret)[\"']?\\s*[:=]/i;\nconst PROSE_LIKE = /[\\r\\n\\t]|\\s{2,}/;\nconst UUID_LIKE =\n /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i;\nconst GT_KEY_LIKE = /gt_(dev|anon|live)_[A-Za-z0-9_-]+/;\n\n/**\n * Inspect a value for unsafe patterns. Returns the matched omission\n * reason or `null` if the value is shape-clean. The function does NOT\n * apply the per-field token regex; that is Layer 2 below.\n *\n * Detector order matters: URL-shape detectors run before token-shape\n * detectors so a URL with an embedded `token=` query parameter is\n * categorized as `raw_payload` (the structural shape that brought it\n * here was the URL, not the token). Header and bearer-shaped values\n * are categorized as `secret` because they always carry credential\n * material. Email is `pii`. Whitespace anomalies are `raw_payload`.\n */\nfunction detectUnsafePattern(value: string): SideEffectOmissionReason | null {\n // Whitespace anomalies fall into raw_payload because they indicate\n // prose or copy-pasted user content rather than a compact label.\n if (value.trim() !== value) return \"raw_payload\";\n if (PROSE_LIKE.test(value)) return \"raw_payload\";\n\n // URL-shape detectors run first so a credential-bearing query\n // string is categorized by its structural shape (raw_payload)\n // rather than the credential token inside it. The product's\n // SCHEMA-036 filter rejects URL shapes regardless of category, so\n // either choice is safe; the test fixture documents that URL-shape\n // wins.\n if (URL_SCHEME.test(value)) return \"raw_payload\";\n if (URL_SCHEME_RELATIVE.test(value)) return \"raw_payload\";\n if (QUERY_LIKE.test(value)) return \"raw_payload\";\n if (FRAGMENT_LIKE.test(value)) return \"raw_payload\";\n\n // Header-shaped and credential-shaped values route to `secret`\n // because they always carry authentication material. Bearer- and\n // token-key-value-like values are detected even when no header\n // prefix is present.\n if (BEARER_LIKE.test(value)) return \"secret\";\n if (HEADER_TOKEN_LIKE.test(value)) return \"secret\";\n if (TOKEN_KV_LIKE.test(value)) return \"secret\";\n if (HEADER_LIKE.test(value)) return \"secret\";\n if (UUID_LIKE.test(value)) return \"secret\";\n if (GT_KEY_LIKE.test(value)) return \"secret\";\n\n // PII detector.\n if (EMAIL_LIKE.test(value)) return \"pii\";\n\n return null;\n}\n\n/**\n * Run Layer 2 per-field validation. Returns `true` when the value\n * matches the field's regex. Length and shape rejection from Layer 1\n * is the caller's responsibility — by the time this runs the value\n * is known to be a non-empty string within the per-field length\n * budget and free of unsafe patterns.\n *\n * Routing order matters: stable-core specialized validators\n * (`locale`, `timezone`) win over the default suffix routing, so\n * stable-core admission is checked first. Pattern-admitted keys\n * route by suffix: `*Count` → digit-only; `*Class` / `*Kind` /\n * `*Role` → compact token. Non-stable-core, non-pattern-matching\n * keys never reach this function (Layer-1 admission via\n * {@link isSideEffectSemanticFieldKey} rejects them and routes to\n * the `unsupported_key` omission counter).\n */\nfunction passesFieldValidator(\n key: SideEffectSemanticFieldKey,\n value: string,\n): boolean {\n if (key === \"locale\") return LOCALE_REGEX.test(value);\n if (key === \"timezone\") return TIMEZONE_REGEX.test(value);\n if (typeof key === \"string\" && key.endsWith(\"Count\")) {\n return DIGIT_REGEX.test(value);\n }\n return TOKEN_REGEX.test(value);\n}\n\n/**\n * Outcome of allowlist enforcement on a single value.\n */\nexport type ValueOutcome =\n | { accepted: true; value: string }\n | { accepted: false; reason: SideEffectOmissionReason };\n\n/**\n * Check the operation label (`recordSideEffect({ operation })`).\n *\n * Order matters: type rejection ⇒ length budget ⇒ unsafe pattern ⇒\n * field validator. This ordering lets each rejection class produce a\n * meaningful omission reason without the regex anchor failure\n * masking an upstream length or shape problem.\n */\nexport function checkOperationLabel(value: unknown): ValueOutcome {\n if (typeof value !== \"string\" || value.length === 0) {\n return { accepted: false, reason: \"raw_payload\" };\n }\n if (value.length > MAX_SIDE_EFFECT_OPERATION_LABEL_LENGTH) {\n return { accepted: false, reason: \"value_too_long\" };\n }\n const unsafe = detectUnsafePattern(value);\n if (unsafe) {\n return { accepted: false, reason: unsafe };\n }\n if (!TOKEN_REGEX.test(value)) {\n // A value that survives unsafe-pattern detection but still fails\n // the compact-token regex is malformed (e.g., starts with a hyphen\n // or contains a slash). Categorize as `raw_payload` because the\n // value carries shape inconsistent with a normalized label.\n return { accepted: false, reason: \"raw_payload\" };\n }\n return { accepted: true, value };\n}\n\n/**\n * Check a semantic field value for one of the allowlisted keys. The\n * key must already be known to be allowlisted (Layer 1 filters\n * unsupported keys via {@link checkSemanticFieldKey}).\n */\nexport function checkSemanticFieldValue(\n key: SideEffectSemanticFieldKey,\n value: unknown,\n): ValueOutcome {\n if (typeof value !== \"string\" || value.length === 0) {\n return { accepted: false, reason: \"raw_payload\" };\n }\n // *Count keys use a tighter length budget than the default\n // field-value cap (integer strings are not free-form tokens).\n // Routing here mirrors the suffix routing in passesFieldValidator.\n const maxLength =\n typeof key === \"string\" && key.endsWith(\"Count\")\n ? MAX_SIDE_EFFECT_FIELD_COUNT_VALUE_LENGTH\n : MAX_SIDE_EFFECT_FIELD_VALUE_LENGTH;\n if (value.length > maxLength) {\n return { accepted: false, reason: \"value_too_long\" };\n }\n const unsafe = detectUnsafePattern(value);\n if (unsafe) {\n return { accepted: false, reason: unsafe };\n }\n if (!passesFieldValidator(key, value)) {\n return { accepted: false, reason: \"raw_payload\" };\n }\n return { accepted: true, value };\n}\n\n/**\n * Returns `true` when `key` is allowlisted as a semantic field key.\n * The narrowing predicate lets callers safely route the value to\n * {@link checkSemanticFieldValue}.\n */\nexport function checkSemanticFieldKey(\n key: unknown,\n): key is SideEffectSemanticFieldKey {\n return typeof key === \"string\" && isSideEffectSemanticFieldKey(key);\n}\n\n/**\n * Returns `true` when `kind` is one of the v1 allowlisted operation\n * kinds.\n */\nexport function checkOperationKind(\n kind: unknown,\n): kind is SideEffectOperationKind {\n return typeof kind === \"string\" && OPERATION_KIND_SET.has(kind);\n}\n\n/**\n * Returns `true` when `status` is one of the v1 allowlisted operation\n * statuses. Distinct from the per-field `status` check because the\n * top-level operation status is enforced as an enum membership only —\n * no compact-token regex applies (the SCHEMA-036 enum is the\n * exhaustive accepted set).\n */\nexport function checkOperationStatus(\n status: unknown,\n): status is SideEffectOperationStatus {\n return typeof status === \"string\" && OPERATION_STATUS_SET.has(status);\n}\n\n/**\n * Returns `true` when `phase` is one of the v1 allowlisted operation\n * phases.\n */\nexport function checkOperationPhase(\n phase: unknown,\n): phase is SideEffectOperationPhase {\n return typeof phase === \"string\" && OPERATION_PHASE_SET.has(phase);\n}\n","/**\n * Side-effect evidence attribute emission and per-span counter state\n * (SDK-049).\n *\n * Pure observer: this module never executes a side effect, never\n * creates a span, never throws to the caller. All failure modes route\n * to silent no-ops or omission counters. Per-span state is held in a\n * `WeakMap` keyed by the OTel `Span` object so counters auto-clean\n * when the span is garbage-collected after export.\n */\n\nimport * as otelApi from \"@opentelemetry/api\";\nimport {\n GLASSTRACE_ATTRIBUTE_NAMES,\n type SideEffectOmissionReason,\n type SideEffectOperationKind,\n type SideEffectOperationPhase,\n type SideEffectOperationStatus,\n type SideEffectSemanticFieldKey,\n} from \"@glasstrace/protocol\";\nimport { MAX_SIDE_EFFECT_OPERATIONS_PER_SPAN } from \"./allowlist.js\";\n\n/**\n * Per-span side-effect bookkeeping. `operationsRecorded` enforces the\n * SCHEMA-036 budget of 5 operations per span; `omissions` carries one\n * counter per omission reason so final `glasstrace.side_effect.omitted.*`\n * attributes can be flushed on the span as integer counts.\n */\ninterface SpanSideEffectState {\n operationsRecorded: number;\n omissions: Map<SideEffectOmissionReason, number>;\n}\n\nconst spanState: WeakMap<otelApi.Span, SpanSideEffectState> = new WeakMap();\n\nfunction getOrCreateState(span: otelApi.Span): SpanSideEffectState {\n let state = spanState.get(span);\n if (!state) {\n state = { operationsRecorded: 0, omissions: new Map() };\n spanState.set(span, state);\n }\n return state;\n}\n\n/**\n * Returns the currently-active span when it is recording and not yet\n * ended. Returns `undefined` when there is no active span, when the\n * active span is a `NonRecordingSpan`, or when the active span has\n * already ended. Callers treat all such cases as silent no-op\n * conditions: there is no span on which to attach evidence.\n */\nfunction getRecordingActiveSpan(): otelApi.Span | undefined {\n let span: otelApi.Span | undefined;\n try {\n span = otelApi.trace.getActiveSpan();\n } catch {\n // Defensive: an OTel API surface error must not propagate to the\n // user's side-effect call site.\n return undefined;\n }\n if (!span) return undefined;\n\n // `isRecording()` returns false for both NonRecordingSpan and ended\n // spans on the standard SDK; honor that as the no-op signal. The\n // method is part of the OTel API contract so a missing impl\n // indicates a host shim — fall through to the conservative no-op.\n try {\n if (typeof span.isRecording === \"function\" && !span.isRecording()) {\n return undefined;\n }\n } catch {\n return undefined;\n }\n return span;\n}\n\n/**\n * Record a single omission count on the active span without emitting\n * the rejected value. Rejection metadata only ever leaks the integer\n * count, never the original input.\n *\n * No-op when there is no recording active span — the rejected value\n * still doesn't reach the wire because emission is gated on a span\n * being available.\n */\nexport function recordOmissionOnActiveSpan(\n reason: SideEffectOmissionReason,\n): void {\n const span = getRecordingActiveSpan();\n if (!span) return;\n recordOmissionOnSpan(span, reason);\n}\n\nfunction recordOmissionOnSpan(\n span: otelApi.Span,\n reason: SideEffectOmissionReason,\n): void {\n const state = getOrCreateState(span);\n const previous = state.omissions.get(reason) ?? 0;\n const next = previous + 1;\n state.omissions.set(reason, next);\n\n const attribute = OMISSION_ATTRIBUTE_BY_REASON[reason];\n try {\n span.setAttribute(attribute, next);\n } catch {\n // OTel may reject the attribute write (slot exhaustion, ended\n // span). The counter still advances in-memory; further emission\n // attempts are harmless no-ops.\n }\n}\n\nconst OMISSION_ATTRIBUTE_BY_REASON: Readonly<\n Record<SideEffectOmissionReason, string>\n> = {\n pii: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_PII,\n secret: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_SECRET,\n raw_payload: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_RAW_PAYLOAD,\n unsupported_key:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_UNSUPPORTED_KEY,\n value_too_long:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_VALUE_TOO_LONG,\n not_emitted: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_NOT_EMITTED,\n capture_disabled:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OMITTED_CAPTURE_DISABLED,\n};\n\n// Stable-core + DISC-1853 keys keep explicit attribute-name constants\n// for backward compatibility with consumers that imported them. New\n// pattern-admitted keys (any `*Class`/`*Count`/`*Kind`/`*Role` matching\n// the open-pattern regex) derive their attribute name at emission via\n// `glasstrace.side_effect.field.${key}` — see `resolveFieldAttribute`\n// below.\nconst FIELD_ATTRIBUTE_BY_KEY: Readonly<Record<string, string>> = {\n templateKey: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_TEMPLATE_KEY,\n providerOperation:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_PROVIDER_OPERATION,\n role: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_ROLE,\n locale: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_LOCALE,\n timezone: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_TIMEZONE,\n status: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_STATUS,\n phase: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_PHASE,\n recipientClass: GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_RECIPIENT_CLASS,\n participantCount:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_PARTICIPANT_COUNT,\n activeParticipantCount:\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_FIELD_ACTIVE_PARTICIPANT_COUNT,\n};\n\n/**\n * Resolve the OTel attribute name for a semantic field key. Keys in\n * the explicit `FIELD_ATTRIBUTE_BY_KEY` map (stable-core + the three\n * DISC-1853 keys) use the existing `GLASSTRACE_ATTRIBUTE_NAMES`\n * constants for backward compatibility. Pattern-admitted keys\n * derive `glasstrace.side_effect.field.${key}` at emission. Callers\n * MUST verify the key is admissible (via `checkSemanticFieldKey` or\n * `isSideEffectSemanticFieldKey`) before calling this function; the\n * regex restricts pattern keys to `[a-zA-Z0-9]` so the derived\n * attribute name is always a safe identifier.\n */\nfunction resolveFieldAttribute(key: string): string {\n const explicit = FIELD_ATTRIBUTE_BY_KEY[key];\n if (explicit !== undefined) return explicit;\n return `glasstrace.side_effect.field.${key}`;\n}\n\n/**\n * Outcome of attempting to attach an operation summary. The\n * `over_budget` and `no_active_span` discriminants let the public API\n * route the call's bookkeeping (omission count vs. silent drop)\n * without re-querying span state.\n */\nexport type AttachOutcome =\n | { kind: \"attached\"; span: otelApi.Span }\n | { kind: \"no_active_span\" }\n | { kind: \"over_budget\"; span: otelApi.Span };\n\n/**\n * Attach the top-level operation attributes to the active span and\n * advance the per-span operation counter. The caller is responsible\n * for invoking {@link attachField} for each accepted semantic field\n * and {@link recordOmission} for each rejected value.\n *\n * Returns the span when emission proceeded, `no_active_span` when no\n * recording span is active, or `over_budget` when the per-span\n * operation budget (5) is exhausted. The caller routes `over_budget`\n * to a `value_too_long` omission via {@link recordOmission}.\n */\nexport function attachOperation(input: {\n kind: SideEffectOperationKind;\n operation: string;\n status?: SideEffectOperationStatus;\n phase?: SideEffectOperationPhase;\n}): AttachOutcome {\n const span = getRecordingActiveSpan();\n if (!span) return { kind: \"no_active_span\" };\n\n const state = getOrCreateState(span);\n if (state.operationsRecorded >= MAX_SIDE_EFFECT_OPERATIONS_PER_SPAN) {\n return { kind: \"over_budget\", span };\n }\n state.operationsRecorded += 1;\n\n try {\n span.setAttribute(GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_KIND, input.kind);\n span.setAttribute(\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_OPERATION,\n input.operation,\n );\n if (input.status !== undefined) {\n span.setAttribute(\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_STATUS,\n input.status,\n );\n }\n if (input.phase !== undefined) {\n span.setAttribute(\n GLASSTRACE_ATTRIBUTE_NAMES.SIDE_EFFECT_PHASE,\n input.phase,\n );\n }\n } catch {\n // Slot exhaustion or ended-span write — ignore. The counter has\n // already advanced; subsequent calls within the same span are\n // budget-bounded as expected.\n }\n return { kind: \"attached\", span };\n}\n\n/**\n * Attach a single allowlisted semantic field to the span. The caller\n * has already routed the field key and value through the allowlist\n * helpers; this function only writes the attribute.\n */\nexport function attachField(\n span: otelApi.Span,\n key: SideEffectSemanticFieldKey,\n value: string,\n): void {\n const attribute = resolveFieldAttribute(key);\n try {\n span.setAttribute(attribute, value);\n } catch {\n // Slot exhaustion — ignore.\n }\n}\n\n/**\n * Record an omission directly on a known span. Used by the public\n * API after {@link attachOperation} returns `over_budget` so the\n * count is registered on the same span that observed the operation.\n */\nexport function recordOmission(\n span: otelApi.Span,\n reason: SideEffectOmissionReason,\n): void {\n recordOmissionOnSpan(span, reason);\n}\n","/**\n * Public side-effect evidence emission API (SDK-049).\n *\n * Exposes {@link recordSideEffect} as a single user-callable function\n * that attaches allowlisted, non-sensitive semantic metadata about a\n * side-effect operation (email, calendar_link, webhook, external_api,\n * queue, after_callback) to the current active OTel span.\n *\n * The behavior contract is observational only: this function never\n * executes a side effect, never retries, never delays, never throws.\n * All failure modes (no active span, ended span, NonRecordingSpan,\n * capture-config disabled, allowlist rejection, per-span budget\n * exhausted, OTel attribute slot exhaustion) silently route to a\n * no-op or to an omission-counter increment that carries no rejected\n * input.\n */\n\nimport type {\n SideEffectOperationKind,\n SideEffectOperationPhase,\n SideEffectOperationStatus,\n SideEffectSemanticFieldKey,\n} from \"@glasstrace/protocol\";\nimport {\n checkOperationKind,\n checkOperationLabel,\n checkOperationPhase,\n checkOperationStatus,\n checkSemanticFieldKey,\n checkSemanticFieldValue,\n} from \"./allowlist.js\";\nimport {\n attachField,\n attachOperation,\n recordOmission,\n recordOmissionOnActiveSpan,\n} from \"./emit.js\";\nimport { getActiveConfig } from \"../init-client.js\";\n\n/**\n * Input shape for {@link recordSideEffect}.\n *\n * All fields except `kind` and `operation` are optional. The SDK\n * silently drops unknown fields and unsafe values, surfacing only an\n * integer omission count under the matching\n * `glasstrace.side_effect.omitted.*` attribute on the active span.\n */\nexport interface RecordSideEffectInput {\n /**\n * One of the allowlisted v1 operation kinds. Calls with any other\n * value (typo, unsupported kind, non-string) silently drop without\n * recording an omission, because there is no kind to attach the\n * counter to.\n */\n kind: SideEffectOperationKind;\n\n /**\n * Compact, normalized operation label (max 96 chars). Must match\n * `^[A-Za-z0-9][A-Za-z0-9_.:-]*$`. Free-form prose, URLs, query\n * strings, and email-shaped values are silently dropped and routed\n * to the matching omission counter.\n */\n operation: string;\n\n /**\n * Optional operation lifecycle status. Defaults to omitted. Values\n * outside the v1 allowlist are silently dropped.\n */\n status?: SideEffectOperationStatus;\n\n /**\n * Optional operation execution phase (request / post_response /\n * background / unknown). Defaults to omitted.\n */\n phase?: SideEffectOperationPhase;\n\n /**\n * Optional allowlisted semantic fields. Keys outside the v1\n * allowlist (`templateKey`, `providerOperation`, `role`, `locale`,\n * `timezone`, `status`, `phase`) and values matching unsafe\n * patterns (URLs, emails, tokens, headers, prose-shaped\n * whitespace) are silently dropped and routed to the matching\n * omission counter.\n */\n fields?: Partial<Record<SideEffectSemanticFieldKey, string>>;\n}\n\n/**\n * Record allowlisted side-effect evidence on the current active OTel\n * span (SDK-049).\n *\n * Behavior is observational only: this function never executes,\n * retries, or duplicates a side effect. The default capture-config\n * flag `sideEffectEvidence` is `false`; callers must opt in via\n * account configuration before any attribute reaches the wire.\n *\n * Edge cases (all silent no-ops):\n * - capture-config flag is `false` ⇒ no-op (no allowlist evaluation)\n * - input is not a plain object ⇒ no-op\n * - `kind` is not in the v1 allowlist ⇒ no-op\n * - no active span ⇒ no-op\n * - active span has already ended or is `NonRecordingSpan` ⇒ no-op\n * - per-span operation budget exhausted (5 ops max) ⇒ records a\n * `value_too_long` omission count, no operation attributes\n * - OTel attribute slot exhaustion ⇒ silently drops the attribute\n * write\n *\n * The SDK guards only callers of this function. Direct\n * `span.setAttribute(\"glasstrace.side_effect.<...>\", ...)` writes\n * bypass the SDK and rely on the product's storage filter (ING-023)\n * as the second defense layer; this is intentional defense-in-depth,\n * not a gap.\n *\n * @example Recording a successful cancellation email\n * ```ts\n * import { recordSideEffect } from \"@glasstrace/sdk\";\n *\n * await mailer.send({ to: recipient, template: \"EventCanceledEmail\" });\n * recordSideEffect({\n * kind: \"email\",\n * operation: \"email.send\",\n * status: \"succeeded\",\n * phase: \"request\",\n * fields: {\n * templateKey: \"EventCanceledEmail\",\n * role: \"invitee\",\n * locale: \"en-US\",\n * timezone: \"Europe/Paris\",\n * },\n * });\n * ```\n */\nexport function recordSideEffect(input: RecordSideEffectInput): void {\n try {\n runRecordSideEffect(input);\n } catch {\n // Defense-in-depth: any unexpected throw inside the function\n // (e.g., a host shim mis-implementing OTel API) must not\n // propagate to the user's code path. Behavior-neutrality requires\n // recordSideEffect to be observationally invisible.\n }\n}\n\nfunction runRecordSideEffect(input: unknown): void {\n if (!input || typeof input !== \"object\") return;\n\n // Capture-config gate: read at every call so config rotation takes\n // effect on the next emission without restart. The disk read is\n // cached inside getActiveConfig() so this stays cheap on the hot\n // path.\n let captureEnabled: boolean;\n try {\n captureEnabled = getActiveConfig().sideEffectEvidence === true;\n } catch {\n captureEnabled = false;\n }\n if (!captureEnabled) {\n // Note: we deliberately do NOT increment a `capture_disabled`\n // omission counter for every call. With the flag off, the SDK's\n // contract is \"no allowlist evaluation runs and no allocation\n // happens\" — surfacing a per-call counter would require attaching\n // to a span and would defeat that goal. The\n // `capture_disabled` reason exists for the receiver-side path\n // where ingestion drops attributes due to product-side flag\n // changes after the SDK emitted them.\n return;\n }\n\n const candidate = input as Partial<RecordSideEffectInput>;\n\n if (!checkOperationKind(candidate.kind)) {\n // No `kind` to attach a counter under — silent drop.\n return;\n }\n\n const labelOutcome = checkOperationLabel(candidate.operation);\n if (!labelOutcome.accepted) {\n recordOmissionOnActiveSpan(labelOutcome.reason);\n return;\n }\n\n let acceptedStatus: SideEffectOperationStatus | undefined;\n if (candidate.status !== undefined) {\n if (checkOperationStatus(candidate.status)) {\n acceptedStatus = candidate.status;\n } else {\n recordOmissionOnActiveSpan(\"unsupported_key\");\n }\n }\n\n let acceptedPhase: SideEffectOperationPhase | undefined;\n if (candidate.phase !== undefined) {\n if (checkOperationPhase(candidate.phase)) {\n acceptedPhase = candidate.phase;\n } else {\n recordOmissionOnActiveSpan(\"unsupported_key\");\n }\n }\n\n const outcome = attachOperation({\n kind: candidate.kind,\n operation: labelOutcome.value,\n status: acceptedStatus,\n phase: acceptedPhase,\n });\n\n if (outcome.kind === \"no_active_span\") {\n // No span to record an omission against either — silent drop.\n return;\n }\n if (outcome.kind === \"over_budget\") {\n recordOmission(outcome.span, \"value_too_long\");\n return;\n }\n\n // Process semantic fields. Each rejection routes to an omission\n // count on the same span; accepted values become field attributes.\n const fields = candidate.fields;\n if (fields && typeof fields === \"object\") {\n for (const [rawKey, rawValue] of Object.entries(fields)) {\n if (!checkSemanticFieldKey(rawKey)) {\n recordOmission(outcome.span, \"unsupported_key\");\n continue;\n }\n const valueOutcome = checkSemanticFieldValue(rawKey, rawValue);\n if (!valueOutcome.accepted) {\n recordOmission(outcome.span, valueOutcome.reason);\n continue;\n }\n attachField(outcome.span, rawKey, valueOutcome.value);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDO,IAAM,yCAAyC;AAS/C,IAAM,qCAAqC;AAQ3C,IAAM,2CAA2C;AASjD,IAAM,sCAAsC;AAEnD,IAAM,qBAA0C,IAAI;AAAA,EAClD;AACF;AACA,IAAM,uBAA4C,IAAI;AAAA,EACpD;AACF;AACA,IAAM,sBAA2C,IAAI;AAAA,EACnD;AACF;AACA,IAAM,sBAA2C,IAAI;AAAA,EACnD;AACF;AAmBA,IAAM,cAAc;AAIpB,IAAM,eAAe;AAIrB,IAAM,iBACJ;AAOF,IAAM,cAAc;AAQpB,IAAM,aAAa;AACnB,IAAM,sBAAsB;AAC5B,IAAM,aAAa;AACnB,IAAM,aAAa;AACnB,IAAM,gBAAgB;AACtB,IAAM,cAAc;AACpB,IAAM,oBACJ;AACF,IAAM,cAAc;AACpB,IAAM,gBACJ;AACF,IAAM,aAAa;AACnB,IAAM,YACJ;AACF,IAAM,cAAc;AAcpB,SAAS,oBAAoB,OAAgD;AAG3E,MAAI,MAAM,KAAK,MAAM,MAAO,QAAO;AACnC,MAAI,WAAW,KAAK,KAAK,EAAG,QAAO;AAQnC,MAAI,WAAW,KAAK,KAAK,EAAG,QAAO;AACnC,MAAI,oBAAoB,KAAK,KAAK,EAAG,QAAO;AAC5C,MAAI,WAAW,KAAK,KAAK,EAAG,QAAO;AACnC,MAAI,cAAc,KAAK,KAAK,EAAG,QAAO;AAMtC,MAAI,YAAY,KAAK,KAAK,EAAG,QAAO;AACpC,MAAI,kBAAkB,KAAK,KAAK,EAAG,QAAO;AAC1C,MAAI,cAAc,KAAK,KAAK,EAAG,QAAO;AACtC,MAAI,YAAY,KAAK,KAAK,EAAG,QAAO;AACpC,MAAI,UAAU,KAAK,KAAK,EAAG,QAAO;AAClC,MAAI,YAAY,KAAK,KAAK,EAAG,QAAO;AAGpC,MAAI,WAAW,KAAK,KAAK,EAAG,QAAO;AAEnC,SAAO;AACT;AAkBA,SAAS,qBACP,KACA,OACS;AACT,MAAI,QAAQ,SAAU,QAAO,aAAa,KAAK,KAAK;AACpD,MAAI,QAAQ,WAAY,QAAO,eAAe,KAAK,KAAK;AACxD,MAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,OAAO,GAAG;AACpD,WAAO,YAAY,KAAK,KAAK;AAAA,EAC/B;AACA,SAAO,YAAY,KAAK,KAAK;AAC/B;AAiBO,SAAS,oBAAoB,OAA8B;AAChE,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG;AACnD,WAAO,EAAE,UAAU,OAAO,QAAQ,cAAc;AAAA,EAClD;AACA,MAAI,MAAM,SAAS,wCAAwC;AACzD,WAAO,EAAE,UAAU,OAAO,QAAQ,iBAAiB;AAAA,EACrD;AACA,QAAM,SAAS,oBAAoB,KAAK;AACxC,MAAI,QAAQ;AACV,WAAO,EAAE,UAAU,OAAO,QAAQ,OAAO;AAAA,EAC3C;AACA,MAAI,CAAC,YAAY,KAAK,KAAK,GAAG;AAK5B,WAAO,EAAE,UAAU,OAAO,QAAQ,cAAc;AAAA,EAClD;AACA,SAAO,EAAE,UAAU,MAAM,MAAM;AACjC;AAOO,SAAS,wBACd,KACA,OACc;AACd,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG;AACnD,WAAO,EAAE,UAAU,OAAO,QAAQ,cAAc;AAAA,EAClD;AAIA,QAAM,YACJ,OAAO,QAAQ,YAAY,IAAI,SAAS,OAAO,IAC3C,2CACA;AACN,MAAI,MAAM,SAAS,WAAW;AAC5B,WAAO,EAAE,UAAU,OAAO,QAAQ,iBAAiB;AAAA,EACrD;AACA,QAAM,SAAS,oBAAoB,KAAK;AACxC,MAAI,QAAQ;AACV,WAAO,EAAE,UAAU,OAAO,QAAQ,OAAO;AAAA,EAC3C;AACA,MAAI,CAAC,qBAAqB,KAAK,KAAK,GAAG;AACrC,WAAO,EAAE,UAAU,OAAO,QAAQ,cAAc;AAAA,EAClD;AACA,SAAO,EAAE,UAAU,MAAM,MAAM;AACjC;AAOO,SAAS,sBACd,KACmC;AACnC,SAAO,OAAO,QAAQ,YAAY,6BAA6B,GAAG;AACpE;AAMO,SAAS,mBACd,MACiC;AACjC,SAAO,OAAO,SAAS,YAAY,mBAAmB,IAAI,IAAI;AAChE;AASO,SAAS,qBACd,QACqC;AACrC,SAAO,OAAO,WAAW,YAAY,qBAAqB,IAAI,MAAM;AACtE;AAMO,SAAS,oBACd,OACmC;AACnC,SAAO,OAAO,UAAU,YAAY,oBAAoB,IAAI,KAAK;AACnE;;;AC7SA,IAAM,YAAwD,oBAAI,QAAQ;AAE1E,SAAS,iBAAiB,MAAyC;AACjE,MAAI,QAAQ,UAAU,IAAI,IAAI;AAC9B,MAAI,CAAC,OAAO;AACV,YAAQ,EAAE,oBAAoB,GAAG,WAAW,oBAAI,IAAI,EAAE;AACtD,cAAU,IAAI,MAAM,KAAK;AAAA,EAC3B;AACA,SAAO;AACT;AASA,SAAS,yBAAmD;AAC1D,MAAI;AACJ,MAAI;AACF,WAAe,MAAM,cAAc;AAAA,EACrC,QAAQ;AAGN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,KAAM,QAAO;AAMlB,MAAI;AACF,QAAI,OAAO,KAAK,gBAAgB,cAAc,CAAC,KAAK,YAAY,GAAG;AACjE,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAWO,SAAS,2BACd,QACM;AACN,QAAM,OAAO,uBAAuB;AACpC,MAAI,CAAC,KAAM;AACX,uBAAqB,MAAM,MAAM;AACnC;AAEA,SAAS,qBACP,MACA,QACM;AACN,QAAM,QAAQ,iBAAiB,IAAI;AACnC,QAAM,WAAW,MAAM,UAAU,IAAI,MAAM,KAAK;AAChD,QAAM,OAAO,WAAW;AACxB,QAAM,UAAU,IAAI,QAAQ,IAAI;AAEhC,QAAM,YAAY,6BAA6B,MAAM;AACrD,MAAI;AACF,SAAK,aAAa,WAAW,IAAI;AAAA,EACnC,QAAQ;AAAA,EAIR;AACF;AAEA,IAAM,+BAEF;AAAA,EACF,KAAK,2BAA2B;AAAA,EAChC,QAAQ,2BAA2B;AAAA,EACnC,aAAa,2BAA2B;AAAA,EACxC,iBACE,2BAA2B;AAAA,EAC7B,gBACE,2BAA2B;AAAA,EAC7B,aAAa,2BAA2B;AAAA,EACxC,kBACE,2BAA2B;AAC/B;AAQA,IAAM,yBAA2D;AAAA,EAC/D,aAAa,2BAA2B;AAAA,EACxC,mBACE,2BAA2B;AAAA,EAC7B,MAAM,2BAA2B;AAAA,EACjC,QAAQ,2BAA2B;AAAA,EACnC,UAAU,2BAA2B;AAAA,EACrC,QAAQ,2BAA2B;AAAA,EACnC,OAAO,2BAA2B;AAAA,EAClC,gBAAgB,2BAA2B;AAAA,EAC3C,kBACE,2BAA2B;AAAA,EAC7B,wBACE,2BAA2B;AAC/B;AAaA,SAAS,sBAAsB,KAAqB;AAClD,QAAM,WAAW,uBAAuB,GAAG;AAC3C,MAAI,aAAa,OAAW,QAAO;AACnC,SAAO,gCAAgC,GAAG;AAC5C;AAwBO,SAAS,gBAAgB,OAKd;AAChB,QAAM,OAAO,uBAAuB;AACpC,MAAI,CAAC,KAAM,QAAO,EAAE,MAAM,iBAAiB;AAE3C,QAAM,QAAQ,iBAAiB,IAAI;AACnC,MAAI,MAAM,sBAAsB,qCAAqC;AACnE,WAAO,EAAE,MAAM,eAAe,KAAK;AAAA,EACrC;AACA,QAAM,sBAAsB;AAE5B,MAAI;AACF,SAAK,aAAa,2BAA2B,kBAAkB,MAAM,IAAI;AACzE,SAAK;AAAA,MACH,2BAA2B;AAAA,MAC3B,MAAM;AAAA,IACR;AACA,QAAI,MAAM,WAAW,QAAW;AAC9B,WAAK;AAAA,QACH,2BAA2B;AAAA,QAC3B,MAAM;AAAA,MACR;AAAA,IACF;AACA,QAAI,MAAM,UAAU,QAAW;AAC7B,WAAK;AAAA,QACH,2BAA2B;AAAA,QAC3B,MAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAIR;AACA,SAAO,EAAE,MAAM,YAAY,KAAK;AAClC;AAOO,SAAS,YACd,MACA,KACA,OACM;AACN,QAAM,YAAY,sBAAsB,GAAG;AAC3C,MAAI;AACF,SAAK,aAAa,WAAW,KAAK;AAAA,EACpC,QAAQ;AAAA,EAER;AACF;AAOO,SAAS,eACd,MACA,QACM;AACN,uBAAqB,MAAM,MAAM;AACnC;;;AC7HO,SAAS,iBAAiB,OAAoC;AACnE,MAAI;AACF,wBAAoB,KAAK;AAAA,EAC3B,QAAQ;AAAA,EAKR;AACF;AAEA,SAAS,oBAAoB,OAAsB;AACjD,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AAMzC,MAAI;AACJ,MAAI;AACF,qBAAiB,gBAAgB,EAAE,uBAAuB;AAAA,EAC5D,QAAQ;AACN,qBAAiB;AAAA,EACnB;AACA,MAAI,CAAC,gBAAgB;AASnB;AAAA,EACF;AAEA,QAAM,YAAY;AAElB,MAAI,CAAC,mBAAmB,UAAU,IAAI,GAAG;AAEvC;AAAA,EACF;AAEA,QAAM,eAAe,oBAAoB,UAAU,SAAS;AAC5D,MAAI,CAAC,aAAa,UAAU;AAC1B,+BAA2B,aAAa,MAAM;AAC9C;AAAA,EACF;AAEA,MAAI;AACJ,MAAI,UAAU,WAAW,QAAW;AAClC,QAAI,qBAAqB,UAAU,MAAM,GAAG;AAC1C,uBAAiB,UAAU;AAAA,IAC7B,OAAO;AACL,iCAA2B,iBAAiB;AAAA,IAC9C;AAAA,EACF;AAEA,MAAI;AACJ,MAAI,UAAU,UAAU,QAAW;AACjC,QAAI,oBAAoB,UAAU,KAAK,GAAG;AACxC,sBAAgB,UAAU;AAAA,IAC5B,OAAO;AACL,iCAA2B,iBAAiB;AAAA,IAC9C;AAAA,EACF;AAEA,QAAM,UAAU,gBAAgB;AAAA,IAC9B,MAAM,UAAU;AAAA,IAChB,WAAW,aAAa;AAAA,IACxB,QAAQ;AAAA,IACR,OAAO;AAAA,EACT,CAAC;AAED,MAAI,QAAQ,SAAS,kBAAkB;AAErC;AAAA,EACF;AACA,MAAI,QAAQ,SAAS,eAAe;AAClC,mBAAe,QAAQ,MAAM,gBAAgB;AAC7C;AAAA,EACF;AAIA,QAAM,SAAS,UAAU;AACzB,MAAI,UAAU,OAAO,WAAW,UAAU;AACxC,eAAW,CAAC,QAAQ,QAAQ,KAAK,OAAO,QAAQ,MAAM,GAAG;AACvD,UAAI,CAAC,sBAAsB,MAAM,GAAG;AAClC,uBAAe,QAAQ,MAAM,iBAAiB;AAC9C;AAAA,MACF;AACA,YAAM,eAAe,wBAAwB,QAAQ,QAAQ;AAC7D,UAAI,CAAC,aAAa,UAAU;AAC1B,uBAAe,QAAQ,MAAM,aAAa,MAAM;AAChD;AAAA,MACF;AACA,kBAAY,QAAQ,MAAM,QAAQ,aAAa,KAAK;AAAA,IACtD;AAAA,EACF;AACF;","names":[]}