autotel-edge 3.17.0 → 3.17.2

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 (57) hide show
  1. package/LICENSE +191 -21
  2. package/README.md +2 -2
  3. package/dist/{config-CEF9DMGT.js → config-CFlfvmQ1.js} +2 -2
  4. package/dist/{config-CEF9DMGT.js.map → config-CFlfvmQ1.js.map} +1 -1
  5. package/dist/events.js +13 -9
  6. package/dist/events.js.map +1 -1
  7. package/dist/{execution-logger-CoxSsBmU.js → execution-logger-CSW2AyVi.js} +7 -4
  8. package/dist/{execution-logger-CoxSsBmU.js.map → execution-logger-CSW2AyVi.js.map} +1 -1
  9. package/dist/index.js +2 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/logger.js +7 -3
  12. package/dist/logger.js.map +1 -1
  13. package/package.json +5 -6
  14. package/src/api/logger.test.ts +0 -1616
  15. package/src/api/logger.ts +0 -1038
  16. package/src/api/redact.test.ts +0 -432
  17. package/src/api/redact.ts +0 -402
  18. package/src/compose.ts +0 -243
  19. package/src/composition.test.ts +0 -130
  20. package/src/composition.ts +0 -132
  21. package/src/core/buffer.ts +0 -17
  22. package/src/core/config.test.ts +0 -413
  23. package/src/core/config.ts +0 -169
  24. package/src/core/context.ts +0 -245
  25. package/src/core/exporter.ts +0 -99
  26. package/src/core/native-bridge.test.ts +0 -176
  27. package/src/core/native-bridge.ts +0 -278
  28. package/src/core/provider.context.test.ts +0 -55
  29. package/src/core/provider.ts +0 -46
  30. package/src/core/span.ts +0 -222
  31. package/src/core/spanprocessor.test.ts +0 -521
  32. package/src/core/spanprocessor.ts +0 -232
  33. package/src/core/trace-context.ts +0 -95
  34. package/src/core/tracer.test.ts +0 -123
  35. package/src/core/tracer.ts +0 -216
  36. package/src/events/index.test.ts +0 -242
  37. package/src/events/index.ts +0 -338
  38. package/src/events.ts +0 -6
  39. package/src/execution-logger.test.ts +0 -368
  40. package/src/execution-logger.ts +0 -410
  41. package/src/functional.test.ts +0 -827
  42. package/src/functional.ts +0 -1002
  43. package/src/index.ts +0 -140
  44. package/src/logger.ts +0 -35
  45. package/src/middleware-toolkit.test.ts +0 -71
  46. package/src/middleware-toolkit.ts +0 -94
  47. package/src/native-routing.test.ts +0 -98
  48. package/src/parse-error.test.ts +0 -70
  49. package/src/parse-error.ts +0 -110
  50. package/src/plugin-runner.test.ts +0 -100
  51. package/src/plugin-runner.ts +0 -215
  52. package/src/sampling/index.test.ts +0 -297
  53. package/src/sampling/index.ts +0 -276
  54. package/src/sampling.ts +0 -6
  55. package/src/testing/index.ts +0 -9
  56. package/src/testing.ts +0 -6
  57. package/src/types.ts +0 -325
package/src/api/redact.ts DELETED
@@ -1,402 +0,0 @@
1
- /**
2
- * Redaction utility for structured log output
3
- *
4
- * Inspired by pino's redaction — zero-dependency, edge-compatible.
5
- * Supports deep paths, wildcards, and custom censor values.
6
- *
7
- * @example
8
- * ```typescript
9
- * const redactor = createRedactor({
10
- * paths: ['password', 'user.email', 'headers.authorization'],
11
- * })
12
- * const safe = redactor({ password: 'secret', user: { email: 'a@b.com' } })
13
- * // { password: '[Redacted]', user: { email: '[Redacted]' } }
14
- * ```
15
- */
16
-
17
- const DEFAULT_CENSOR = '[Redacted]';
18
-
19
- /**
20
- * Validate and parse a dot/bracket path string into segments.
21
- *
22
- * Valid: "user.email", "users[*].name", "headers.*", "[0].secret"
23
- * Invalid: "user..email", "user[", "foo.]bar", ""
24
- */
25
- function parsePath(path: string): string[] {
26
- if (typeof path !== 'string' || path.length === 0) {
27
- throw new Error(
28
- `redact path must be a non-empty string, got: ${JSON.stringify(path)}`,
29
- );
30
- }
31
-
32
- // Reject obviously malformed paths
33
- if (path.includes('..')) {
34
- throw new Error(
35
- `redact path contains empty segment: ${JSON.stringify(path)}`,
36
- );
37
- }
38
- // Unmatched brackets: "[" with no "]" or vice versa
39
- const openBracket = path.indexOf('[');
40
- const closeBracket = path.indexOf(']');
41
- if (
42
- (openBracket !== -1 && closeBracket === -1) ||
43
- (closeBracket !== -1 && openBracket === -1)
44
- ) {
45
- throw new Error(
46
- `redact path has unclosed bracket: ${JSON.stringify(path)}`,
47
- );
48
- }
49
- if (openBracket !== -1 && closeBracket < openBracket) {
50
- throw new Error(
51
- `redact path has bracket close before open: ${JSON.stringify(path)}`,
52
- );
53
- }
54
- if (/\[\s*\]$/.test(path)) {
55
- throw new Error(
56
- `redact path has unclosed bracket: ${JSON.stringify(path)}`,
57
- );
58
- }
59
- if (/\][^.[\]]/.test(path) && !/\]\[/.test(path)) {
60
- // e.g. "foo.]bar" — closing bracket not followed by dot, bracket, or end
61
- const afterClose = path.match(/\]([^.[\]]+)/);
62
- if (afterClose) {
63
- throw new Error(
64
- `redact path has invalid characters after ']': ${JSON.stringify(path)}`,
65
- );
66
- }
67
- }
68
-
69
- const segments: string[] = [];
70
- const rx = /[^.[\]]+|\[(\d+|\*)\]/g;
71
- let match: RegExpExecArray | null;
72
- let lastIndex = 0;
73
- while ((match = rx.exec(path)) !== null) {
74
- segments.push(match[1] ?? match[0]);
75
- lastIndex = rx.lastIndex;
76
- }
77
-
78
- // If the regex didn't consume the whole string, something is malformed
79
- if (lastIndex < path.length) {
80
- throw new Error(
81
- `redact path has unexpected trailing content: ${JSON.stringify(path)}`,
82
- );
83
- }
84
-
85
- if (segments.length === 0) {
86
- throw new Error(
87
- `redact path produced no segments: ${JSON.stringify(path)}`,
88
- );
89
- }
90
-
91
- return segments;
92
- }
93
-
94
- function isPlainObject(value: unknown): value is Record<string, unknown> {
95
- if (value === null || typeof value !== 'object') return false;
96
- const proto = Object.getPrototypeOf(value);
97
- return proto === Object.prototype || proto === null;
98
- }
99
-
100
- /**
101
- * Deep clone plain objects and arrays only.
102
- * Non-plain objects (Date, Map, Set, class instances) are returned by reference.
103
- */
104
- function deepClone<T>(value: T): T {
105
- if (value === null || typeof value !== 'object') return value;
106
- if (Array.isArray(value)) return value.map((v) => deepClone(v)) as T;
107
- if (!isPlainObject(value)) return value;
108
- const result: Record<string, unknown> = {};
109
- for (const key of Object.keys(value)) {
110
- result[key] = deepClone((value as Record<string, unknown>)[key]);
111
- }
112
- return result as T;
113
- }
114
-
115
- /**
116
- * A node in the compiled path tree.
117
- *
118
- * Unlike a simple PathTree | null representation, a node can be both
119
- * a redact target AND a parent for deeper paths. This correctly handles
120
- * overlapping paths like ['user', 'user.email'].
121
- */
122
- interface PathNode {
123
- redact?: boolean;
124
- children?: Record<string, PathNode>;
125
- }
126
-
127
- /**
128
- * Insert a parsed path into the tree.
129
- *
130
- * buildPathTree([['user'], ['user', 'email']]) produces:
131
- * { user: { redact: true, children: { email: { redact: true } } } }
132
- */
133
- function buildPathTree(paths: string[][]): PathNode {
134
- const root: PathNode = {};
135
- for (const segments of paths) {
136
- let node = root;
137
- for (let i = 0; i < segments.length; i++) {
138
- const seg = segments[i];
139
- const isLast = i === segments.length - 1;
140
-
141
- if (!node.children) node.children = {};
142
- if (!node.children[seg]) node.children[seg] = {};
143
-
144
- if (isLast) {
145
- node.children[seg].redact = true;
146
- }
147
- node = node.children[seg];
148
- }
149
- }
150
- return root;
151
- }
152
-
153
- /**
154
- * Walk a compiled path tree and redact matching values in `obj`.
155
- * Mutates `obj` in place (caller should clone before calling).
156
- */
157
- function redactWithTree(
158
- obj: Record<string, any>,
159
- node: PathNode,
160
- censor: Censor,
161
- remove: boolean,
162
- path: string[] = [],
163
- ): void {
164
- if (!node.children) return;
165
-
166
- const applyCensor = (target: Record<string, any>, k: string, currentPath: string[]) => {
167
- if (remove) {
168
- delete target[k];
169
- } else {
170
- target[k] = typeof censor === 'function' ? censor(target[k], currentPath) : censor;
171
- }
172
- };
173
-
174
- for (const [key, childNode] of Object.entries(node.children)) {
175
- if (key === '*') {
176
- for (const k of Object.keys(obj)) {
177
- const wildcardPath = [...path, k];
178
- if (childNode.redact && !childNode.children) {
179
- applyCensor(obj, k, wildcardPath);
180
- } else if (childNode.redact && childNode.children) {
181
- const child = obj[k];
182
- if (
183
- child != null &&
184
- typeof child === 'object' &&
185
- !Array.isArray(child)
186
- ) {
187
- redactWithTree(child, childNode, censor, remove, wildcardPath);
188
- } else {
189
- applyCensor(obj, k, wildcardPath);
190
- }
191
- } else if (childNode.children) {
192
- const child = obj[k];
193
- if (child != null && typeof child === 'object') {
194
- redactWithTree(child, childNode, censor, remove, wildcardPath);
195
- }
196
- }
197
- }
198
- continue;
199
- }
200
-
201
- if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
202
-
203
- const val = obj[key];
204
- const currentPath = [...path, key];
205
-
206
- if (childNode.redact && !childNode.children) {
207
- applyCensor(obj, key, currentPath);
208
- } else if (childNode.redact && childNode.children) {
209
- if (val != null && typeof val === 'object') {
210
- redactWithTree(val, childNode, censor, remove, currentPath);
211
- }
212
- } else if (childNode.children && val != null && typeof val === 'object') {
213
- redactWithTree(val, childNode, censor, remove, currentPath);
214
- }
215
- }
216
- }
217
-
218
- export type Censor = string | ((value: unknown, path: string[]) => unknown);
219
-
220
- export interface RedactorOptions {
221
- paths: string[];
222
- censor?: Censor;
223
- remove?: boolean;
224
- }
225
-
226
- /**
227
- * Preset name or explicit options
228
- */
229
- export type RedactorConfig = RedactorOptions | RedactorPreset;
230
-
231
- export type RedactorPreset = 'default' | 'strict' | 'pci-dss';
232
-
233
- /**
234
- * Paths that match the main autotel package's `sensitiveKey` pattern:
235
- * `/^(password|passwd|pwd|secret|token|api[_-]?key|auth|credential|private[_-]?key|authorization)$/i`
236
- *
237
- * Copied here so the edge logger stays zero-dependency.
238
- */
239
- const SENSITIVE_KEY_PATHS = [
240
- 'password',
241
- 'passwd',
242
- 'pwd',
243
- 'secret',
244
- 'token',
245
- 'apiKey',
246
- 'api_key',
247
- 'auth',
248
- 'credential',
249
- 'privateKey',
250
- 'private_key',
251
- 'authorization',
252
- ];
253
-
254
- /**
255
- * Common request header paths that often carry secrets.
256
- */
257
- const SENSITIVE_HEADER_PATHS = [
258
- 'req.headers.authorization',
259
- 'req.headers.cookie',
260
- 'headers.authorization',
261
- 'headers.cookie',
262
- 'request.headers.authorization',
263
- 'request.headers.cookie',
264
- ];
265
-
266
- /**
267
- * Built-in redactor presets.
268
- *
269
- * - `default` — sensitive keys + request headers (passwords, tokens, secrets, auth headers)
270
- * - `strict` — default + bearer/jwt/api-key patterns nested at any depth
271
- * - `pci-dss` — credit card and payment-related fields
272
- */
273
- export const REDACT_PRESETS: Record<RedactorPreset, RedactorOptions> = {
274
- /**
275
- * Default: covers the same sensitive-key names as the main autotel package's
276
- * `sensitiveKey` pattern, plus common request header paths.
277
- *
278
- * Redacted keys: password, passwd, pwd, secret, token, apiKey, auth,
279
- * credential, privateKey, authorization
280
- * Redacted headers: req.headers.authorization, req.headers.cookie, etc.
281
- */
282
- default: {
283
- paths: [...SENSITIVE_KEY_PATHS, ...SENSITIVE_HEADER_PATHS],
284
- },
285
-
286
- /**
287
- * Strict: everything in default, plus nested paths for bearer tokens,
288
- * JWT fields, and API key fields at common depths.
289
- */
290
- strict: {
291
- paths: [
292
- ...SENSITIVE_KEY_PATHS,
293
- ...SENSITIVE_HEADER_PATHS,
294
- // Common nested auth shapes
295
- 'bearer',
296
- 'jwt',
297
- 'apiSecret',
298
- 'api_secret',
299
- 'accessToken',
300
- 'access_token',
301
- 'refreshToken',
302
- 'refresh_token',
303
- 'clientSecret',
304
- 'client_secret',
305
- ],
306
- },
307
-
308
- /**
309
- * PCI-DSS: credit card and payment-related fields.
310
- */
311
- 'pci-dss': {
312
- paths: [
313
- 'card',
314
- 'cardNumber',
315
- 'card_number',
316
- 'ccn',
317
- 'pan',
318
- 'cvv',
319
- 'cvc',
320
- 'expirationDate',
321
- 'expiration_date',
322
- 'exp',
323
- 'payment.card',
324
- 'payment.cardNumber',
325
- 'payment.cvv',
326
- ],
327
- },
328
- };
329
-
330
- /**
331
- * Resolve a preset name or explicit options into RedactorOptions.
332
- */
333
- function resolveConfig(config: RedactorConfig): RedactorOptions {
334
- if (typeof config === 'string') {
335
- const preset = REDACT_PRESETS[config];
336
- if (!preset) {
337
- throw new Error(
338
- `Unknown redactor preset: "${config}". Available: ${Object.keys(REDACT_PRESETS).join(', ')}`,
339
- );
340
- }
341
- return preset;
342
- }
343
- return config;
344
- }
345
-
346
- /**
347
- * Create a redactor function from a list of paths.
348
- *
349
- * Paths use dot notation for nested fields and `[*]` or `*` for wildcards:
350
- * - `"password"` — top-level key
351
- * - `"user.email"` — nested field
352
- * - `"users[*].password"` or `"users.*.password"` — all items in array/object
353
- * - `"headers.authorization"` — nested header
354
- * - `"[*].secret"` — wildcard at root
355
- *
356
- * Overlapping paths are supported — e.g. `['user', 'user.email']` will
357
- * redact `user` as a whole while still recursing into `user.email`.
358
- *
359
- * The returned function clones plain objects/arrays before mutating,
360
- * so the original is never modified. Non-plain objects (Date, Map, etc.)
361
- * are passed through by reference.
362
- *
363
- * @example
364
- * ```typescript
365
- * // Using a preset
366
- * const redactor = createRedactor('default')
367
- *
368
- * // Using explicit paths
369
- * const redactor = createRedactor({
370
- * paths: ['password', 'token', 'user.email', 'users[*].ssn'],
371
- * censor: '[Filtered]', // optional, default '[Redacted]'
372
- * })
373
- *
374
- * redactor({ password: 's3cret', user: { email: 'a@b.com' } })
375
- * // → { password: '[Filtered]', user: { email: '[Filtered]' } }
376
- *
377
- * // Custom censor function:
378
- * const mask = createRedactor({
379
- * paths: ['ccn'],
380
- * censor: (val) => '****' + String(val).slice(-4),
381
- * })
382
- * ```
383
- */
384
- export function createRedactor(config: RedactorConfig): <T>(obj: T) => T {
385
- const { paths, censor = DEFAULT_CENSOR, remove = false } = resolveConfig(config);
386
-
387
- if (paths.length === 0) {
388
- return <T>(obj: T): T => obj;
389
- }
390
-
391
- const parsed = paths.map((p) => parsePath(p));
392
- const tree = buildPathTree(parsed);
393
-
394
- return function redactor<T>(obj: T): T {
395
- if (obj == null || typeof obj !== 'object') return obj;
396
-
397
- // Clone so we don't mutate the original log entry
398
- const clone = deepClone(obj) as Record<string, any>;
399
- redactWithTree(clone, tree, censor, remove);
400
- return clone as T;
401
- };
402
- }
package/src/compose.ts DELETED
@@ -1,243 +0,0 @@
1
- /**
2
- * Composition utilities for autotel-edge
3
- *
4
- * Helper functions for composing instrumentation and middleware.
5
- *
6
- * @example
7
- * ```typescript
8
- * import { compose } from 'autotel-edge/api/compose'
9
- * import { instrumentGlobalFetch, instrumentGlobalCache } from 'autotel-edge/instrumentation'
10
- *
11
- * const setupInstrumentation = compose(
12
- * instrumentGlobalFetch,
13
- * instrumentGlobalCache
14
- * )
15
- *
16
- * setupInstrumentation({ enabled: true })
17
- * ```
18
- */
19
-
20
- /**
21
- * Compose multiple setup functions into one
22
- *
23
- * Takes multiple instrumentation functions and returns a single function
24
- * that calls them all in order with the same config.
25
- *
26
- * @example
27
- * ```typescript
28
- * const setup = compose(
29
- * instrumentGlobalFetch,
30
- * instrumentGlobalCache
31
- * )
32
- *
33
- * setup({ enabled: true })
34
- * ```
35
- */
36
- export function compose<TConfig = unknown>(
37
- ...fns: Array<(config?: TConfig) => void>
38
- ): (config?: TConfig) => void {
39
- return (config?: TConfig) => {
40
- for (const fn of fns) {
41
- fn(config);
42
- }
43
- };
44
- }
45
-
46
- /**
47
- * Compose multiple async setup functions into one
48
- *
49
- * Like `compose` but for async functions.
50
- *
51
- * @example
52
- * ```typescript
53
- * const setup = composeAsync(
54
- * async (config) => { await initTracing(config) },
55
- * async (config) => { await initMetrics(config) }
56
- * )
57
- *
58
- * await setup({ endpoint: 'https://...' })
59
- * ```
60
- */
61
- export function composeAsync<TConfig = unknown>(
62
- ...fns: Array<(config?: TConfig) => Promise<void>>
63
- ): (config?: TConfig) => Promise<void> {
64
- return async (config?: TConfig) => {
65
- for (const fn of fns) {
66
- await fn(config);
67
- }
68
- };
69
- }
70
-
71
- /**
72
- * Pipe - compose functions from left to right
73
- *
74
- * Unlike `compose` which is right-to-left, pipe is left-to-right
75
- * which matches the execution order visually.
76
- *
77
- * @example
78
- * ```typescript
79
- * const setup = pipe(
80
- * (config) => ({ ...config, tracing: true }),
81
- * (config) => ({ ...config, metrics: true }),
82
- * (config) => initObservability(config)
83
- * )
84
- *
85
- * setup({ service: 'my-worker' })
86
- * ```
87
- */
88
- export function pipe<TInput, TOutput>(
89
- ...fns: Array<(input: any) => any>
90
- ): (input: TInput) => TOutput {
91
- return (input: TInput) => {
92
- return fns.reduce((acc, fn) => fn(acc), input as any) as TOutput;
93
- };
94
- }
95
-
96
- /**
97
- * Create a conditional instrumentation function
98
- *
99
- * Only runs the instrumentation if the predicate returns true.
100
- *
101
- * @example
102
- * ```typescript
103
- * const setupFetch = when(
104
- * (env) => env.ENABLE_FETCH_TRACING === 'true',
105
- * instrumentGlobalFetch
106
- * )
107
- *
108
- * setupFetch(env)
109
- * ```
110
- */
111
- export function when<TConfig = unknown>(
112
- predicate: (config?: TConfig) => boolean,
113
- fn: (config?: TConfig) => void
114
- ): (config?: TConfig) => void {
115
- return (config?: TConfig) => {
116
- if (predicate(config)) {
117
- fn(config);
118
- }
119
- };
120
- }
121
-
122
- /**
123
- * Create a conditional async instrumentation function
124
- *
125
- * @example
126
- * ```typescript
127
- * const setupCache = whenAsync(
128
- * async (env) => await featureEnabled('cache-tracing'),
129
- * instrumentGlobalCache
130
- * )
131
- *
132
- * await setupCache(env)
133
- * ```
134
- */
135
- export function whenAsync<TConfig = unknown>(
136
- predicate: (config?: TConfig) => Promise<boolean> | boolean,
137
- fn: (config?: TConfig) => Promise<void>
138
- ): (config?: TConfig) => Promise<void> {
139
- return async (config?: TConfig) => {
140
- if (await predicate(config)) {
141
- await fn(config);
142
- }
143
- };
144
- }
145
-
146
- /**
147
- * Tap - run a side effect and return the original value
148
- *
149
- * Useful for logging or debugging in a pipe.
150
- *
151
- * @example
152
- * ```typescript
153
- * const setup = pipe(
154
- * tap((config) => console.log('Initial config:', config)),
155
- * (config) => ({ ...config, tracing: true }),
156
- * tap((config) => console.log('After tracing:', config)),
157
- * (config) => initObservability(config)
158
- * )
159
- * ```
160
- */
161
- export function tap<T>(fn: (value: T) => void): (value: T) => T {
162
- return (value: T) => {
163
- fn(value);
164
- return value;
165
- };
166
- }
167
-
168
- /**
169
- * Memoize - cache the result of a function
170
- *
171
- * Useful for expensive initialization functions that should only run once.
172
- *
173
- * @example
174
- * ```typescript
175
- * const setup = memoize(() => {
176
- * console.log('Setting up (expensive)...')
177
- * instrumentGlobalFetch()
178
- * instrumentGlobalCache()
179
- * })
180
- *
181
- * setup() // Logs "Setting up (expensive)..."
182
- * setup() // Does nothing (cached)
183
- * setup() // Does nothing (cached)
184
- * ```
185
- */
186
- export function memoize<TArgs extends any[], TReturn>(
187
- fn: (...args: TArgs) => TReturn
188
- ): (...args: TArgs) => TReturn {
189
- const cached: { hasValue: boolean; value: TReturn } = { hasValue: false, value: undefined as any };
190
-
191
- return (...args: TArgs) => {
192
- if (!cached.hasValue) {
193
- cached.value = fn(...args);
194
- cached.hasValue = true;
195
- }
196
- return cached.value;
197
- };
198
- }
199
-
200
- /**
201
- * Retry - retry a function on failure
202
- *
203
- * @example
204
- * ```typescript
205
- * const setupWithRetry = retry(
206
- * async () => {
207
- * await fetch('https://api.example.com/init')
208
- * },
209
- * { maxAttempts: 3, delayMs: 1000 }
210
- * )
211
- *
212
- * await setupWithRetry()
213
- * ```
214
- */
215
- export function retry<TArgs extends any[], TReturn>(
216
- fn: (...args: TArgs) => Promise<TReturn>,
217
- options: {
218
- maxAttempts?: number;
219
- delayMs?: number;
220
- onRetry?: (attempt: number, error: Error) => void;
221
- } = {}
222
- ): (...args: TArgs) => Promise<TReturn> {
223
- const { maxAttempts = 3, delayMs = 1000, onRetry } = options;
224
-
225
- return async (...args: TArgs) => {
226
- let lastError: Error | undefined;
227
-
228
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
229
- try {
230
- return await fn(...args);
231
- } catch (error) {
232
- lastError = error instanceof Error ? error : new Error(String(error));
233
-
234
- if (attempt < maxAttempts) {
235
- onRetry?.(attempt, lastError);
236
- await new Promise(resolve => setTimeout(resolve, delayMs));
237
- }
238
- }
239
- }
240
-
241
- throw lastError;
242
- };
243
- }