@debugbundle/sdk-react-native 3.0.1 → 3.1.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 (84) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/DebugBundleReactNative.podspec +2 -2
  3. package/MIGRATION-3.1.md +116 -0
  4. package/README.md +37 -1
  5. package/android/build.gradle +1 -1
  6. package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativeModule.java +34 -3
  7. package/app.plugin.js +1 -1
  8. package/dist/NativeDebugBundleReactNative.d.ts +1 -0
  9. package/dist/NativeDebugBundleReactNative.d.ts.map +1 -1
  10. package/dist/before-send.d.ts.map +1 -1
  11. package/dist/before-send.js +9 -131
  12. package/dist/canonical-schema.d.ts +15 -0
  13. package/dist/canonical-schema.d.ts.map +1 -0
  14. package/dist/canonical-schema.js +139 -0
  15. package/dist/client-values.d.ts +17 -0
  16. package/dist/client-values.d.ts.map +1 -0
  17. package/dist/client-values.js +185 -0
  18. package/dist/client.d.ts +7 -0
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +102 -205
  21. package/dist/diagnostic-budget.d.ts +16 -0
  22. package/dist/diagnostic-budget.d.ts.map +1 -0
  23. package/dist/diagnostic-budget.js +120 -0
  24. package/dist/event-schema-data.d.ts +2 -0
  25. package/dist/event-schema-data.d.ts.map +1 -0
  26. package/dist/event-schema-data.js +49 -0
  27. package/dist/evidence-capabilities.d.ts +5 -0
  28. package/dist/evidence-capabilities.d.ts.map +1 -0
  29. package/dist/evidence-capabilities.js +21 -0
  30. package/dist/evidence-controller.d.ts +24 -0
  31. package/dist/evidence-controller.d.ts.map +1 -0
  32. package/dist/evidence-controller.js +113 -0
  33. package/dist/evidence-types.d.ts +33 -0
  34. package/dist/evidence-types.d.ts.map +1 -0
  35. package/dist/evidence-types.js +1 -0
  36. package/dist/evidence-wire.d.ts +7 -0
  37. package/dist/evidence-wire.d.ts.map +1 -0
  38. package/dist/evidence-wire.js +132 -0
  39. package/dist/exception-evidence.d.ts +25 -0
  40. package/dist/exception-evidence.d.ts.map +1 -0
  41. package/dist/exception-evidence.js +175 -0
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +4 -0
  45. package/dist/owned-json.d.ts +11 -0
  46. package/dist/owned-json.d.ts.map +1 -0
  47. package/dist/owned-json.js +85 -0
  48. package/dist/privacy-keys.d.ts +2 -0
  49. package/dist/privacy-keys.d.ts.map +1 -1
  50. package/dist/privacy-keys.js +10 -2
  51. package/dist/privacy-telemetry.js +7 -7
  52. package/dist/privacy-text.d.ts +2 -2
  53. package/dist/privacy-text.d.ts.map +1 -1
  54. package/dist/privacy-text.js +35 -33
  55. package/dist/redaction.d.ts.map +1 -1
  56. package/dist/redaction.js +9 -7
  57. package/dist/safe-error-fields.d.ts +4 -0
  58. package/dist/safe-error-fields.d.ts.map +1 -0
  59. package/dist/safe-error-fields.js +54 -0
  60. package/dist/types.d.ts +21 -6
  61. package/dist/types.d.ts.map +1 -1
  62. package/ios/DebugBundleReactNative.swift +58 -4
  63. package/ios/DebugBundleReactNativeBridge.mm +4 -0
  64. package/package.json +3 -2
  65. package/src/NativeDebugBundleReactNative.ts +1 -0
  66. package/src/before-send.ts +10 -174
  67. package/src/canonical-schema.ts +108 -0
  68. package/src/client-values.ts +196 -0
  69. package/src/client.ts +92 -213
  70. package/src/diagnostic-budget.ts +90 -0
  71. package/src/event-schema-data.ts +49 -0
  72. package/src/evidence-capabilities.ts +19 -0
  73. package/src/evidence-controller.ts +89 -0
  74. package/src/evidence-types.ts +14 -0
  75. package/src/evidence-wire.ts +100 -0
  76. package/src/exception-evidence.ts +112 -0
  77. package/src/index.ts +6 -0
  78. package/src/owned-json.ts +60 -0
  79. package/src/privacy-keys.ts +12 -2
  80. package/src/privacy-telemetry.ts +9 -9
  81. package/src/privacy-text.ts +32 -31
  82. package/src/redaction.ts +8 -7
  83. package/src/safe-error-fields.ts +46 -0
  84. package/src/types.ts +23 -5
@@ -0,0 +1,25 @@
1
+ type LegacyError = {
2
+ name: string;
3
+ message: string;
4
+ stack: string;
5
+ traceAvailable?: boolean;
6
+ };
7
+ type Frame = {
8
+ function: string | null;
9
+ file: string | null;
10
+ line: number | null;
11
+ column: number | null;
12
+ classification: "unknown" | "framework" | "sdk";
13
+ };
14
+ export declare function snapshotError(error: unknown, fields: string[]): LegacyError;
15
+ export declare function parseMobileStack(stack: string, maximum?: number): {
16
+ frames: Frame[];
17
+ status: string;
18
+ truncated: boolean;
19
+ };
20
+ /** New cause traversal requires the engine's native Error brand. Hermes/JSC
21
+ * without it retain the outer error and explicitly report unsupported traversal.
22
+ * Portable JS cannot prove aggregate arrays are not proxies: no iterator is run. */
23
+ export declare function snapshotExceptionEvidence(error: unknown, legacy: LegacyError, fields: string[], fatal?: boolean, brand?: ((value: unknown) => boolean) | undefined): Record<string, unknown> | undefined;
24
+ export {};
25
+ //# sourceMappingURL=exception-evidence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exception-evidence.d.ts","sourceRoot":"","sources":["../src/exception-evidence.ts"],"names":[],"mappings":"AAIA,KAAK,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAC9F,KAAK,KAAK,GAAG;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,cAAc,EAAE,SAAS,GAAG,WAAW,GAAG,KAAK,CAAA;CAAE,CAAC;AAI3J,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,WAAW,CAc3E;AAUD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG;IAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CA0BrH;AAUD;;oFAEoF;AACpF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,KAAK,UAAQ,EAC5G,KAAK,4CAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAsC1D"}
@@ -0,0 +1,175 @@
1
+ import { errorField, safeErrorStack, nativeBrand } from "./safe-error-fields.js";
2
+ import { sanitizeValue } from "./redaction.js";
3
+ import { utf8Bytes } from "./owned-json.js";
4
+ const text = (value, fallback) => typeof value === "string" && value ? value : fallback;
5
+ export function snapshotError(error, fields) {
6
+ let name = "Error", message = "Unknown error", stack;
7
+ try {
8
+ if (typeof error === "string")
9
+ message = error;
10
+ else if (error !== null && typeof error === "object") {
11
+ name = text(errorField(error, "name"), name);
12
+ message = text(errorField(error, "message"), message);
13
+ stack = safeErrorStack(error);
14
+ }
15
+ }
16
+ catch { /* Legacy outer evidence is best effort; no application accessor is required. */ }
17
+ return {
18
+ name: text(sanitizeValue(name, { redactFields: fields }), "Error"),
19
+ message: text(sanitizeValue(message, { redactFields: fields }), "Unknown error"),
20
+ stack: text(sanitizeValue(text(stack, `${name}: ${message}`), { redactFields: fields, maxStringLength: 8192 }), `${name}: ${message}`),
21
+ traceAvailable: typeof stack === "string" && stack.length > 0
22
+ };
23
+ }
24
+ function clip(text, maximum) {
25
+ let result = "", bytes = 0;
26
+ for (const character of text.slice(0, maximum)) {
27
+ const cost = utf8Bytes(character);
28
+ if (bytes + cost > maximum)
29
+ break;
30
+ result += character;
31
+ bytes += cost;
32
+ }
33
+ return result;
34
+ }
35
+ export function parseMobileStack(stack, maximum = 32) {
36
+ const frames = [];
37
+ const lines = stack.slice(0, 16_384).split(/\r?\n/, 129);
38
+ let partial = false, truncated = stack.length > 16_384 || lines.length > 128;
39
+ for (const input of lines.slice(0, 128)) {
40
+ if (frames.length >= maximum) {
41
+ truncated = true;
42
+ break;
43
+ }
44
+ if (input.length > 2048)
45
+ truncated = true;
46
+ const line = input.trim().slice(0, 2048);
47
+ const named = /^at\s+(.+?)\s*\((?:address at )?(.+?):(\d+)(?::(\d+))?\)$/.exec(line);
48
+ const anonymous = named ? null : /^at\s+(.+?):(\d+)(?::(\d+))?$/.exec(line);
49
+ const webkit = named || anonymous ? null : /^(.*?)@(.+?):(\d+)(?::(\d+))?$/.exec(line);
50
+ const match = named ?? anonymous ?? webkit;
51
+ if (!match) {
52
+ if (/^at\s|@\S+:\d/.test(line))
53
+ partial = true;
54
+ continue;
55
+ }
56
+ let file = named || webkit ? match[2] : match[1];
57
+ if (/^https?:\/\//i.test(file)) {
58
+ try {
59
+ const url = new URL(file);
60
+ file = `${url.protocol}//${url.host}${url.pathname}`;
61
+ }
62
+ catch {
63
+ partial = true;
64
+ continue;
65
+ }
66
+ }
67
+ else
68
+ file = file.split(/[?#]/, 1)[0];
69
+ const row = Number(named || webkit ? match[3] : match[2]);
70
+ const rawColumn = named || webkit ? match[4] : match[3];
71
+ const column = rawColumn === undefined ? null : Number(rawColumn);
72
+ if (!Number.isSafeInteger(row) || row <= 0 || column !== null && (!Number.isSafeInteger(column) || column < 0)) {
73
+ partial = true;
74
+ continue;
75
+ }
76
+ const fn = named || webkit ? clip(match[1].trim(), 256) || null : null;
77
+ frames.push({ function: fn, file: clip(file, 2048), line: row, column,
78
+ classification: /node_modules[/\\]@debugbundle[/\\]/.test(file) ? "sdk" : /node_modules[/\\]/.test(file) ? "framework" : "unknown" });
79
+ }
80
+ return { frames, status: truncated ? "truncated" : !frames.length ? "unparsed" : partial ? "partial" : "parsed", truncated };
81
+ }
82
+ function stackLinks(stack) {
83
+ return stack.replace(/https?:\/\/[^\s)]+/gi, source => {
84
+ const coordinate = /(:\d+(?::\d+)?)$/.exec(source)?.[1] ?? "";
85
+ const location = coordinate ? source.slice(0, -coordinate.length) : source;
86
+ try {
87
+ const url = new URL(location);
88
+ return `${url.protocol}//${url.host}${url.pathname}${coordinate}`;
89
+ }
90
+ catch {
91
+ return "[unavailable URL]";
92
+ }
93
+ });
94
+ }
95
+ /** New cause traversal requires the engine's native Error brand. Hermes/JSC
96
+ * without it retain the outer error and explicitly report unsupported traversal.
97
+ * Portable JS cannot prove aggregate arrays are not proxies: no iterator is run. */
98
+ export function snapshotExceptionEvidence(error, legacy, fields, fatal = false, brand = nativeBrand) {
99
+ const nodes = [], seen = new Map(), active = new Set();
100
+ let unsupported = brand === undefined, unsafe = false, truncated = false, totalFrames = 0;
101
+ const visit = (value, depth, root = false) => {
102
+ if (!root && (!brand || !brand(value))) {
103
+ unsupported = true;
104
+ return null;
105
+ }
106
+ if (typeof value === "object" && value !== null && active.has(value)) {
107
+ truncated = true;
108
+ return null;
109
+ }
110
+ if (depth > (fatal ? 1 : 4) || nodes.length >= (fatal ? 2 : 8)) {
111
+ truncated = true;
112
+ return null;
113
+ }
114
+ if (!root && seen.has(value))
115
+ return seen.get(value);
116
+ const snapshot = root ? legacy : snapshotError(value, fields);
117
+ const name = clip(snapshot.name, 256), message = clip(snapshot.message, fatal ? 512 : 2048);
118
+ const stack = stackLinks(clip(snapshot.stack, fatal ? 512 : 16_384));
119
+ const parsed = parseMobileStack(stack, fatal ? Math.min(4, 8 - totalFrames) : 32), frames = parsed.frames;
120
+ totalFrames += frames.length;
121
+ truncated ||= parsed.truncated;
122
+ const trace = snapshot.traceAvailable !== false;
123
+ const id = nodes.length;
124
+ const node = { id, name, message, stack: trace ? stack : null, frames: trace ? frames : [],
125
+ trace_origin: trace ? "runtime_reported" : "unavailable", parsing_status: trace ? parsed.status : "unavailable", relationships: [] };
126
+ nodes.push(node);
127
+ if (!brand || !brand(value)) {
128
+ unsupported = true;
129
+ return id;
130
+ }
131
+ seen.set(value, id);
132
+ active.add(value);
133
+ const cause = Object.getOwnPropertyDescriptor(value, "cause");
134
+ if (cause && !("value" in cause))
135
+ unsafe = true;
136
+ else if (cause?.value !== undefined) {
137
+ const target = visit(cause.value, depth + 1);
138
+ if (target !== null)
139
+ node.relationships.push({ kind: "cause", target_id: target });
140
+ }
141
+ const aggregate = Object.getOwnPropertyDescriptor(value, "errors");
142
+ if (aggregate) {
143
+ if (!("value" in aggregate))
144
+ unsafe = true;
145
+ else
146
+ unsupported = true;
147
+ }
148
+ active.delete(value);
149
+ return id;
150
+ };
151
+ try {
152
+ visit(error, 0, true);
153
+ }
154
+ catch {
155
+ unsafe = true;
156
+ }
157
+ if (!nodes.length)
158
+ return undefined;
159
+ const graph = { version: 1, runtime: "react_native", root_id: 0, nodes, truncated,
160
+ unavailable_reason: unsafe ? "unsafe" : truncated ? "budget_exceeded" : unsupported ? "unsupported" : null };
161
+ const maximum = fatal ? 3584 : 31_000;
162
+ if (utf8Bytes(JSON.stringify(graph)) > maximum) {
163
+ graph.truncated = true;
164
+ graph.unavailable_reason = "budget_exceeded";
165
+ for (const node of nodes) {
166
+ node.stack = null;
167
+ node.frames = node.frames.slice(0, fatal ? 2 : 4);
168
+ node.parsing_status = "truncated";
169
+ }
170
+ }
171
+ if (utf8Bytes(JSON.stringify(graph)) > maximum)
172
+ for (const node of nodes)
173
+ node.frames = [];
174
+ return utf8Bytes(JSON.stringify(graph)) <= maximum ? graph : undefined;
175
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,12 @@ import { DebugBundleReactNativeClient, createDebugBundleClient } from "./client.
2
2
  import { captureDebugBundleConsole } from "./console.js";
3
3
  import { installDebugBundleErrorHandlers } from "./errors.js";
4
4
  import type { DebugBundleCaptureContext, DebugBundleBeforeSend, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus } from "./types.js";
5
+ import type { EvidenceCapabilities, OperationScope, RecoveryObservation } from "./evidence-types.js";
5
6
  export declare const DebugBundle: {
7
+ beginOperation(): OperationScope;
8
+ withOperationContext<T>(operation: (scope: OperationScope) => T): T;
9
+ captureRecovery(observation: RecoveryObservation): void;
10
+ readonly evidenceCapabilities: EvidenceCapabilities;
6
11
  init(config: DebugBundleConfig): void;
7
12
  captureException(error: unknown, context?: DebugBundleCaptureContext): void;
8
13
  captureError(error: unknown, context?: DebugBundleCaptureContext): void;
@@ -21,5 +26,6 @@ export declare const DebugBundle: {
21
26
  export { createDebugBundleClient, DebugBundleReactNativeClient };
22
27
  export { captureDebugBundleConsole };
23
28
  export { installDebugBundleErrorHandlers };
29
+ export type { EvidenceCapabilities, EvidenceFeature, EvidenceScope, OperationScope, RecoveryObservation } from "./evidence-types.js";
24
30
  export type { DebugBundleCaptureContext, DebugBundleBeforeSend, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus };
25
31
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EACV,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,WAAW;iBACT,iBAAiB,GAAG,IAAI;4BAWb,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGvD,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGnD,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;4BAG3E,sBAAsB,YAAY,uBAAuB,YAAY,yBAAyB,GAAG,IAAI;4BAGrG,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;oBAGvF,MAAM,SAAS,OAAO,GAAG,IAAI;iBAGhC,MAAM,QAAQ,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,uBAAuB,GAAG,IAAI;aAGrF,OAAO,CAAC,IAAI,CAAC;qCAGW,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;2BAGnC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;6BAG3C,MAAM,mBAAmB,MAAM,GAAG,IAAI,WAAW,MAAM,GAAG,IAAI;qBAGzE,iBAAiB;0BAGZ,MAAM,GAAG,IAAI;CAGjC,CAAC;AAEF,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,CAAC;AACjE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AACrC,OAAO,EAAE,+BAA+B,EAAE,CAAC;AAC3C,YAAY,EACV,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EACV,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAIrG,eAAO,MAAM,WAAW;sBACJ,cAAc;yBACX,CAAC,aAAa,CAAC,KAAK,EAAE,cAAc,KAAK,CAAC,GAAG,CAAC;iCACtC,mBAAmB,GAAG,IAAI;mCAC3B,oBAAoB;iBACnC,iBAAiB,GAAG,IAAI;4BAWb,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGvD,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGnD,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;4BAG3E,sBAAsB,YAAY,uBAAuB,YAAY,yBAAyB,GAAG,IAAI;4BAGrG,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;oBAGvF,MAAM,SAAS,OAAO,GAAG,IAAI;iBAGhC,MAAM,QAAQ,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,uBAAuB,GAAG,IAAI;aAGrF,OAAO,CAAC,IAAI,CAAC;qCAGW,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;2BAGnC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;6BAG3C,MAAM,mBAAmB,MAAM,GAAG,IAAI,WAAW,MAAM,GAAG,IAAI;qBAGzE,iBAAiB;0BAGZ,MAAM,GAAG,IAAI;CAGjC,CAAC;AAEF,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,CAAC;AACjE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AACrC,OAAO,EAAE,+BAA+B,EAAE,CAAC;AAC3C,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AACrI,YAAY,EACV,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,CAAC"}
package/dist/index.js CHANGED
@@ -3,6 +3,10 @@ import { captureDebugBundleConsole } from "./console.js";
3
3
  import { installDebugBundleErrorHandlers } from "./errors.js";
4
4
  const singleton = new DebugBundleReactNativeClient();
5
5
  export const DebugBundle = {
6
+ beginOperation() { return singleton.beginOperation(); },
7
+ withOperationContext(operation) { return singleton.withOperationContext(operation); },
8
+ captureRecovery(observation) { singleton.captureRecovery(observation); },
9
+ get evidenceCapabilities() { return singleton.evidenceCapabilities; },
6
10
  init(config) {
7
11
  singleton.init(config);
8
12
  if (config.captureErrors !== false) {
@@ -0,0 +1,11 @@
1
+ export type OwnedJSON = null | boolean | number | string | OwnedJSON[] | {
2
+ [key: string]: OwnedJSON;
3
+ };
4
+ /** Exhausted ownership cannot safely fall back to pre-hook application data. */
5
+ export declare class EventValidationUnavailable extends Error {
6
+ }
7
+ export declare function utf8Bytes(value: string): number;
8
+ /** Bounded plain JSON copy; no toJSON, property getters, foreign iterators or formatters. */
9
+ export declare function ownJSON(input: unknown): OwnedJSON;
10
+ export declare function jsonObject(value: unknown): Record<string, OwnedJSON> | null;
11
+ //# sourceMappingURL=owned-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-json.d.ts","sourceRoot":"","sources":["../src/owned-json.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEtG,gFAAgF;AAChF,qBAAa,0BAA2B,SAAQ,KAAK;CAAG;AAExD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAW/C;AAED,6FAA6F;AAC7F,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAoCjD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,IAAI,CAE3E"}
@@ -0,0 +1,85 @@
1
+ /** Exhausted ownership cannot safely fall back to pre-hook application data. */
2
+ export class EventValidationUnavailable extends Error {
3
+ }
4
+ export function utf8Bytes(value) {
5
+ let bytes = 0;
6
+ for (let index = 0; index < value.length; index++) {
7
+ const code = value.charCodeAt(index);
8
+ if (code < 0x80)
9
+ bytes++;
10
+ else if (code < 0x800)
11
+ bytes += 2;
12
+ else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length &&
13
+ value.charCodeAt(index + 1) >= 0xdc00 && value.charCodeAt(index + 1) <= 0xdfff) {
14
+ bytes += 4;
15
+ index++;
16
+ }
17
+ else
18
+ bytes += 3;
19
+ }
20
+ return bytes;
21
+ }
22
+ /** Bounded plain JSON copy; no toJSON, property getters, foreign iterators or formatters. */
23
+ export function ownJSON(input) {
24
+ let nodes = 4096, remaining = 8 * 1024 * 1024;
25
+ const seen = new Set();
26
+ const use = (amount) => { remaining -= amount; if (remaining < 0)
27
+ throw new EventValidationUnavailable(); };
28
+ const visit = (value, depth) => {
29
+ if (--nodes < 0 || depth > 16)
30
+ throw new EventValidationUnavailable();
31
+ if (value === null || typeof value === "boolean") {
32
+ use(5);
33
+ return value;
34
+ }
35
+ if (typeof value === "number" && Number.isFinite(value)) {
36
+ use(24);
37
+ return value;
38
+ }
39
+ if (typeof value === "string") {
40
+ if (value.length > remaining)
41
+ throw new EventValidationUnavailable();
42
+ use(utf8Bytes(value) + 2);
43
+ return value;
44
+ }
45
+ if (typeof value !== "object" || value === null || seen.has(value))
46
+ throw new TypeError("invalid JSON");
47
+ seen.add(value);
48
+ try {
49
+ if (Array.isArray(value)) {
50
+ if (value.length > 256)
51
+ throw new EventValidationUnavailable();
52
+ use(value.length + 2);
53
+ return Array.from({ length: value.length }, (_, index) => {
54
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
55
+ if (!descriptor || !("value" in descriptor))
56
+ throw new TypeError("invalid JSON");
57
+ return visit(descriptor.value, depth + 1);
58
+ });
59
+ }
60
+ if (![Object.prototype, null].includes(Object.getPrototypeOf(value)))
61
+ throw new TypeError("invalid JSON");
62
+ const keys = Object.keys(value);
63
+ if (keys.length > 256)
64
+ throw new EventValidationUnavailable();
65
+ const output = Object.create(null);
66
+ for (const key of keys) {
67
+ if (key.length > remaining)
68
+ throw new EventValidationUnavailable();
69
+ use(utf8Bytes(key) + 4);
70
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
71
+ if (!descriptor || !("value" in descriptor))
72
+ throw new TypeError("invalid JSON");
73
+ output[key] = visit(descriptor.value, depth + 1);
74
+ }
75
+ return output;
76
+ }
77
+ finally {
78
+ seen.delete(value);
79
+ }
80
+ };
81
+ return visit(input, 0);
82
+ }
83
+ export function jsonObject(value) {
84
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
85
+ }
@@ -1,3 +1,5 @@
1
1
  export declare const DEFAULT_SENSITIVE_KEYS: readonly ["password", "secret", "token", "api_key", "apikey", "access_token", "refresh_token", "private_key", "authorization", "bearer", "cookie", "session_id", "passwd", "ssn", "credit_card", "card_number", "cvv", "cvc", "pin", "expiry", "phone", "otp", "verification_code"];
2
2
  export declare function isSensitiveKey(key: string, sensitiveKeys: readonly string[]): boolean;
3
+ /** Operation-owned lookup: bounded caller policy never enters a global cache. */
4
+ export declare function createSensitiveKeyMatcher(sensitiveKeys: readonly string[]): (key: string) => boolean;
3
5
  //# sourceMappingURL=privacy-keys.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"privacy-keys.d.ts","sourceRoot":"","sources":["../src/privacy-keys.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,qRAKzB,CAAC;AAeX,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAkBrF"}
1
+ {"version":3,"file":"privacy-keys.d.ts","sourceRoot":"","sources":["../src/privacy-keys.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,qRAKzB,CAAC;AAeX,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAErF;AAED,iFAAiF;AACjF,wBAAgB,yBAAyB,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAIpG"}
@@ -16,11 +16,19 @@ function splitKeyIntoSegments(key) {
16
16
  .filter((segment) => segment.length > 0);
17
17
  }
18
18
  export function isSensitiveKey(key, sensitiveKeys) {
19
+ return createSensitiveKeyMatcher(sensitiveKeys)(key);
20
+ }
21
+ /** Operation-owned lookup: bounded caller policy never enters a global cache. */
22
+ export function createSensitiveKeyMatcher(sensitiveKeys) {
23
+ const exact = new Set(sensitiveKeys);
24
+ const canonicalSensitiveKeys = new Set(sensitiveKeys.map(canonicalizeSensitiveKey));
25
+ return key => matchesSensitiveKey(key, exact, canonicalSensitiveKeys);
26
+ }
27
+ function matchesSensitiveKey(key, exact, canonicalSensitiveKeys) {
19
28
  const normalized = key.trim().toLowerCase();
20
- if (sensitiveKeys.includes(normalized)) {
29
+ if (exact.has(normalized)) {
21
30
  return true;
22
31
  }
23
- const canonicalSensitiveKeys = new Set(sensitiveKeys.map(canonicalizeSensitiveKey));
24
32
  const segments = splitKeyIntoSegments(key);
25
33
  for (let start = 0; start < segments.length; start += 1) {
26
34
  let combined = "";
@@ -1,5 +1,5 @@
1
- import { DEFAULT_SENSITIVE_KEYS, isSensitiveKey } from "./privacy-keys.js";
2
- import { scrubCredentialText } from "./privacy-text.js";
1
+ import { DEFAULT_SENSITIVE_KEYS, createSensitiveKeyMatcher } from "./privacy-keys.js";
2
+ import { createCredentialTextScrubber } from "./privacy-text.js";
3
3
  const REDACTED = "[REDACTED]";
4
4
  const MAX_DEPTH = 16;
5
5
  const MAX_NODES = 4096;
@@ -56,7 +56,7 @@ function sanitizeString(value, state, allowStructured) {
56
56
  // Malformed structured content with a recognizable sensitive label is withheld below.
57
57
  }
58
58
  }
59
- const result = scrubCredentialText(value, state.extra);
59
+ const result = state.scrubText(value);
60
60
  if (result !== value)
61
61
  state.redactionCount += 1;
62
62
  if ((value.startsWith("{") || value.startsWith("[")) && result === value &&
@@ -108,7 +108,7 @@ function sanitizeValue(value, state, depth, allowStructured) {
108
108
  state.textBytes += byteLength(key);
109
109
  if (state.textBytes > state.maxTotalBytes)
110
110
  throw new BudgetExceeded();
111
- const safeKey = scrubCredentialText(key, state.extra);
111
+ const safeKey = state.scrubText(key);
112
112
  if (safeKey !== key) {
113
113
  state.redactionCount += 1;
114
114
  continue;
@@ -116,7 +116,7 @@ function sanitizeValue(value, state, depth, allowStructured) {
116
116
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
117
117
  if (descriptor === undefined || !("value" in descriptor))
118
118
  throw new TypeError("unsafe_input");
119
- const safeValue = isSensitiveKey(key, state.keys)
119
+ const safeValue = state.sensitiveKey(key)
120
120
  ? mask(state)
121
121
  : sanitizeValue(descriptor.value, state, depth + 1, allowStructured);
122
122
  Object.defineProperty(result, key, {
@@ -148,8 +148,8 @@ export function sanitizeTelemetry(value, options = {}) {
148
148
  textBytes: 0,
149
149
  redactionCount: 0,
150
150
  seen: new WeakSet(),
151
- keys: [...DEFAULT_SENSITIVE_KEYS, ...extra],
152
- extra,
151
+ sensitiveKey: createSensitiveKeyMatcher([...DEFAULT_SENSITIVE_KEYS, ...extra]),
152
+ scrubText: createCredentialTextScrubber(extra),
153
153
  maxTotalBytes
154
154
  };
155
155
  const sanitized = sanitizeValue(value, state, 0, true);
@@ -1,3 +1,3 @@
1
- /** Bounded caller input only. Returns stable replacement text, never the matching secret. */
2
- export declare function scrubCredentialText(input: string, additionalKeys: readonly string[], scanUrls?: boolean): string;
1
+ /** Compile each caller rule once per bounded protection operation, not per scalar. */
2
+ export declare function createCredentialTextScrubber(additionalKeys: readonly string[]): (input: string, scanUrls?: boolean) => string;
3
3
  //# sourceMappingURL=privacy-text.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"privacy-text.d.ts","sourceRoot":"","sources":["../src/privacy-text.ts"],"names":[],"mappings":"AAyDA,6FAA6F;AAC7F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,UAAO,GAAG,MAAM,CA2B7G"}
1
+ {"version":3,"file":"privacy-text.d.ts","sourceRoot":"","sources":["../src/privacy-text.ts"],"names":[],"mappings":"AAyDA,sFAAsF;AACtF,wBAAgB,4BAA4B,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,MAAM,CA4B7H"}
@@ -1,4 +1,4 @@
1
- import { DEFAULT_SENSITIVE_KEYS, isSensitiveKey } from "./privacy-keys.js";
1
+ import { DEFAULT_SENSITIVE_KEYS, createSensitiveKeyMatcher } from "./privacy-keys.js";
2
2
  const REDACTED = "[REDACTED]";
3
3
  const BUILTIN_LABELS = [...DEFAULT_SENSITIVE_KEYS, "client_secret", "x_api_key",
4
4
  "accessToken", "refreshToken", "privateKey", "clientSecret"];
@@ -29,7 +29,7 @@ function isValidCard(value) {
29
29
  }
30
30
  return sum % 10 === 0;
31
31
  }
32
- function scrubUrl(raw, keys) {
32
+ function scrubUrl(raw, scrub, sensitive) {
33
33
  try {
34
34
  const url = new URL(raw);
35
35
  if (url.username || url.password) {
@@ -38,9 +38,9 @@ function scrubUrl(raw, keys) {
38
38
  }
39
39
  const nextQuery = new URLSearchParams();
40
40
  for (const [key, value] of url.searchParams) {
41
- if (key.length > 128 || scrubCredentialText(key, keys, false) !== key)
41
+ if (key.length > 128 || scrub(key, false) !== key)
42
42
  continue;
43
- nextQuery.append(key, isSensitiveKey(key, keys) || scrubCredentialText(value, keys, false) !== value ? REDACTED : value);
43
+ nextQuery.append(key, sensitive(key) || scrub(value, false) !== value ? REDACTED : value);
44
44
  }
45
45
  url.search = nextQuery.toString();
46
46
  url.hash = "";
@@ -51,35 +51,37 @@ function scrubUrl(raw, keys) {
51
51
  return REDACTED;
52
52
  }
53
53
  }
54
- /** Bounded caller input only. Returns stable replacement text, never the matching secret. */
55
- export function scrubCredentialText(input, additionalKeys, scanUrls = true) {
56
- const keys = [...DEFAULT_SENSITIVE_KEYS, ...additionalKeys];
57
- if (PEM_START.test(input) && !PEM_COMPLETE.test(input))
58
- return REDACTED;
59
- let output = input;
60
- if (/(?:password|token|secret|authorization|cookie)%3[ad]/i.test(output)) {
61
- try {
62
- output = decodeURIComponent(output);
63
- }
64
- catch {
54
+ /** Compile each caller rule once per bounded protection operation, not per scalar. */
55
+ export function createCredentialTextScrubber(additionalKeys) {
56
+ const sensitive = createSensitiveKeyMatcher([...DEFAULT_SENSITIVE_KEYS, ...additionalKeys]);
57
+ const patterns = additionalKeys.map(key => {
58
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
59
+ return new RegExp(`\\b(${escaped})\\b(["']?\\s*[:=]\\s*)(?:"[^"]*"|'[^']*'|[^\\s&,;]+)`, "gi");
60
+ });
61
+ const scrub = (input, scanUrls = true) => {
62
+ if (PEM_START.test(input) && !PEM_COMPLETE.test(input))
65
63
  return REDACTED;
64
+ let output = input;
65
+ if (/(?:password|token|secret|authorization|cookie)%3[ad]/i.test(output)) {
66
+ try {
67
+ output = decodeURIComponent(output);
68
+ }
69
+ catch {
70
+ return REDACTED;
71
+ }
66
72
  }
67
- }
68
- output = output.replace(PEM, REDACTED).replace(HEADER, "$1: [REDACTED]");
69
- output = output.replace(BEARER, "$1 [REDACTED]").replace(DEBUGBUNDLE_TOKEN, REDACTED);
70
- output = output.replace(ASSIGNMENT, "$1$2[REDACTED]");
71
- if (additionalKeys.length > 0) {
72
- for (const key of additionalKeys) {
73
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
74
- const named = new RegExp(`\\b(${escaped})\\b(["']?\\s*[:=]\\s*)(?:"[^"]*"|'[^']*'|[^\\s&,;]+)`, "gi");
75
- output = output.replace(named, "$1$2[REDACTED]");
76
- }
77
- }
78
- output = output.replace(CARD, (candidate) => isValidCard(candidate) ? REDACTED : candidate);
79
- if (!scanUrls)
80
- return output;
81
- return output.replace(URL_TEXT, (matched) => {
82
- const trailing = matched.match(/[).,;]+$/)?.[0] ?? "";
83
- return scrubUrl(matched.slice(0, matched.length - trailing.length), keys) + trailing;
84
- });
73
+ output = output.replace(PEM, REDACTED).replace(HEADER, "$1: [REDACTED]");
74
+ output = output.replace(BEARER, "$1 [REDACTED]").replace(DEBUGBUNDLE_TOKEN, REDACTED);
75
+ output = output.replace(ASSIGNMENT, "$1$2[REDACTED]");
76
+ for (const pattern of patterns)
77
+ output = output.replace(pattern, "$1$2[REDACTED]");
78
+ output = output.replace(CARD, (candidate) => isValidCard(candidate) ? REDACTED : candidate);
79
+ if (!scanUrls)
80
+ return output;
81
+ return output.replace(URL_TEXT, (matched) => {
82
+ const trailing = matched.match(/[).,;]+$/)?.[0] ?? "";
83
+ return scrubUrl(matched.slice(0, matched.length - trailing.length), scrub, sensitive) + trailing;
84
+ });
85
+ };
86
+ return scrub;
85
87
  }
@@ -1 +1 @@
1
- {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,wBAAwB,UAQpC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAE9C;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAUpF;AAED,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAC3C,SAAS,GAAE,MAAM,EAA6B,EAC9C,OAAO,GAAE,eAAoB,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAaxB"}
1
+ {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,wBAAwB,UAQpC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAE9C;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAUpF;AAED,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAC3C,SAAS,GAAE,MAAM,EAA6B,EAC9C,OAAO,GAAE,eAAoB,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAaxB"}
package/dist/redaction.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_SENSITIVE_KEYS, isSensitiveKey as privacySensitiveKey } from "./privacy-keys.js";
2
2
  import { sanitizeTelemetry } from "./privacy-telemetry.js";
3
+ import { errorField, safeErrorStack } from "./safe-error-fields.js";
3
4
  const DEFAULT_REDACT_FIELDS = [
4
5
  ...DEFAULT_SENSITIVE_KEYS
5
6
  ];
@@ -69,9 +70,6 @@ function sanitize(value, options, depth, visited, key) {
69
70
  if (typeof value === "function" || typeof value === "symbol") {
70
71
  return `[${typeof value}]`;
71
72
  }
72
- if (value instanceof Error) {
73
- return sanitizeError(value, options, depth, visited);
74
- }
75
73
  if (depth >= options.maxDepth) {
76
74
  return "[Truncated: depth]";
77
75
  }
@@ -80,6 +78,8 @@ function sanitize(value, options, depth, visited, key) {
80
78
  return "[Circular]";
81
79
  }
82
80
  visited.add(value);
81
+ if (value instanceof Error)
82
+ return sanitizeError(value, options, depth, visited);
83
83
  if (Array.isArray(value)) {
84
84
  return value
85
85
  .slice(0, options.maxArrayLength)
@@ -94,11 +94,13 @@ function sanitize(value, options, depth, visited, key) {
94
94
  return String(value);
95
95
  }
96
96
  function sanitizeError(error, options, depth, visited) {
97
+ const name = errorField(error, "name"), message = errorField(error, "message");
98
+ const stack = safeErrorStack(error), cause = Object.getOwnPropertyDescriptor(error, "cause");
97
99
  return {
98
- name: truncate(error.name || "Error", options.maxStringLength, options.redactFields),
99
- message: truncate(error.message || "", options.maxStringLength, options.redactFields),
100
- stack: typeof error.stack === "string" ? truncate(error.stack, options.maxStringLength * 4, options.redactFields) : null,
101
- cause: "cause" in error ? sanitize(error.cause, options, depth + 1, visited, "cause") : null
100
+ name: truncate(typeof name === "string" && name ? name : "Error", options.maxStringLength, options.redactFields),
101
+ message: truncate(typeof message === "string" ? message : "", options.maxStringLength, options.redactFields),
102
+ stack: typeof stack === "string" ? truncate(stack, options.maxStringLength * 4, options.redactFields) : null,
103
+ cause: cause && "value" in cause ? sanitize(cause.value, options, depth + 1, visited, "cause") : null
102
104
  };
103
105
  }
104
106
  function truncate(value, maxLength, fields) {
@@ -0,0 +1,4 @@
1
+ export declare const nativeBrand: ((value: unknown) => boolean) | undefined;
2
+ export declare function errorField(value: object, key: string): unknown;
3
+ export declare function safeErrorStack(value: object): unknown;
4
+ //# sourceMappingURL=safe-error-fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-error-fields.d.ts","sourceRoot":"","sources":["../src/safe-error-fields.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,WAAW,WACa,OAAO,KAAK,OAAO,aAAY,CAAC;AAGrE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAW9D;AACD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAyBrD"}