@aforoai/metering 1.0.0 → 2.0.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 (91) hide show
  1. package/README.md +71 -25
  2. package/dist/cjs/client.d.ts +33 -35
  3. package/dist/cjs/client.d.ts.map +1 -1
  4. package/dist/cjs/client.js +193 -147
  5. package/dist/cjs/client.js.map +1 -1
  6. package/dist/cjs/idempotency.d.ts +9 -9
  7. package/dist/cjs/idempotency.js +9 -9
  8. package/dist/cjs/index.d.ts +8 -8
  9. package/dist/cjs/index.d.ts.map +1 -1
  10. package/dist/cjs/index.js +20 -19
  11. package/dist/cjs/index.js.map +1 -1
  12. package/dist/cjs/limits.d.ts +47 -7
  13. package/dist/cjs/limits.d.ts.map +1 -1
  14. package/dist/cjs/limits.js +65 -10
  15. package/dist/cjs/limits.js.map +1 -1
  16. package/dist/cjs/middleware/common.d.ts +24 -2
  17. package/dist/cjs/middleware/common.d.ts.map +1 -1
  18. package/dist/cjs/middleware/common.js +66 -4
  19. package/dist/cjs/middleware/common.js.map +1 -1
  20. package/dist/cjs/middleware/express.d.ts +11 -6
  21. package/dist/cjs/middleware/express.d.ts.map +1 -1
  22. package/dist/cjs/middleware/express.js +25 -62
  23. package/dist/cjs/middleware/express.js.map +1 -1
  24. package/dist/cjs/middleware/fastify.d.ts +9 -5
  25. package/dist/cjs/middleware/fastify.d.ts.map +1 -1
  26. package/dist/cjs/middleware/fastify.js +28 -48
  27. package/dist/cjs/middleware/fastify.js.map +1 -1
  28. package/dist/cjs/middleware/koa.d.ts +11 -6
  29. package/dist/cjs/middleware/koa.d.ts.map +1 -1
  30. package/dist/cjs/middleware/koa.js +28 -46
  31. package/dist/cjs/middleware/koa.js.map +1 -1
  32. package/dist/cjs/transport-core/core.d.ts +355 -0
  33. package/dist/cjs/transport-core/core.d.ts.map +1 -0
  34. package/dist/cjs/transport-core/core.js +1047 -0
  35. package/dist/cjs/transport-core/core.js.map +1 -0
  36. package/dist/cjs/types.d.ts +108 -20
  37. package/dist/cjs/types.d.ts.map +1 -1
  38. package/dist/esm/client.js +191 -145
  39. package/dist/esm/client.js.map +1 -1
  40. package/dist/esm/idempotency.js +9 -9
  41. package/dist/esm/index.js +7 -7
  42. package/dist/esm/index.js.map +1 -1
  43. package/dist/esm/limits.js +62 -10
  44. package/dist/esm/limits.js.map +1 -1
  45. package/dist/esm/middleware/common.js +62 -4
  46. package/dist/esm/middleware/common.js.map +1 -1
  47. package/dist/esm/middleware/express.js +24 -61
  48. package/dist/esm/middleware/express.js.map +1 -1
  49. package/dist/esm/middleware/fastify.js +27 -47
  50. package/dist/esm/middleware/fastify.js.map +1 -1
  51. package/dist/esm/middleware/koa.js +27 -45
  52. package/dist/esm/middleware/koa.js.map +1 -1
  53. package/dist/esm/package.json +1 -0
  54. package/dist/esm/transport-core/core.js +1038 -0
  55. package/dist/esm/transport-core/core.js.map +1 -0
  56. package/dist/types/client.d.ts +33 -35
  57. package/dist/types/client.d.ts.map +1 -1
  58. package/dist/types/idempotency.d.ts +9 -9
  59. package/dist/types/index.d.ts +8 -8
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/types/limits.d.ts +47 -7
  62. package/dist/types/limits.d.ts.map +1 -1
  63. package/dist/types/middleware/common.d.ts +24 -2
  64. package/dist/types/middleware/common.d.ts.map +1 -1
  65. package/dist/types/middleware/express.d.ts +11 -6
  66. package/dist/types/middleware/express.d.ts.map +1 -1
  67. package/dist/types/middleware/fastify.d.ts +9 -5
  68. package/dist/types/middleware/fastify.d.ts.map +1 -1
  69. package/dist/types/middleware/koa.d.ts +11 -6
  70. package/dist/types/middleware/koa.d.ts.map +1 -1
  71. package/dist/types/transport-core/core.d.ts +355 -0
  72. package/dist/types/transport-core/core.d.ts.map +1 -0
  73. package/dist/types/types.d.ts +108 -20
  74. package/dist/types/types.d.ts.map +1 -1
  75. package/package.json +22 -6
  76. package/dist/cjs/buffer.d.ts +0 -27
  77. package/dist/cjs/buffer.d.ts.map +0 -1
  78. package/dist/cjs/buffer.js +0 -75
  79. package/dist/cjs/buffer.js.map +0 -1
  80. package/dist/cjs/transport.d.ts +0 -39
  81. package/dist/cjs/transport.d.ts.map +0 -1
  82. package/dist/cjs/transport.js +0 -116
  83. package/dist/cjs/transport.js.map +0 -1
  84. package/dist/esm/buffer.js +0 -71
  85. package/dist/esm/buffer.js.map +0 -1
  86. package/dist/esm/transport.js +0 -112
  87. package/dist/esm/transport.js.map +0 -1
  88. package/dist/types/buffer.d.ts +0 -27
  89. package/dist/types/buffer.d.ts.map +0 -1
  90. package/dist/types/transport.d.ts +0 -39
  91. package/dist/types/transport.d.ts.map +0 -1
@@ -5,8 +5,10 @@
5
5
  * billed. The server reports it per event (`errors[]` in the batch response,
6
6
  * indexed), so the rest of the batch survives — but the SDK sends in the
7
7
  * background, so that report reaches nobody: the usage is simply gone. Checking
8
- * the same limits at `track()` surfaces the problem to the caller, at the call
9
- * site that produced it, while the event can still be corrected.
8
+ * the same limits at `track()` reports the problem where it happens: the event
9
+ * is not buffered or sent, it is counted in `droppedCount`, WARN-logged with
10
+ * the field, the limit and the offending value, and handed to the opt-in
11
+ * `onDrop` hook with reason `'invalid'`. `track()` does not throw for it.
10
12
  *
11
13
  * Source: `dto/IngestUsageEventRequest` (aforo-nextgen-usage-ingestor-service) —
12
14
  * the `@Size` and `@Digits` bean constraints, which are compiled into the
@@ -21,9 +23,15 @@
21
23
  * Only `occurredAt`'s shape is checked, since a malformed timestamp is invalid
22
24
  * under every configuration.
23
25
  *
24
- * Nothing here truncates or rounds — that would change what is billed. The
25
- * offending event is rejected instead, naming the field, the limit, and the
26
- * offending value.
26
+ * The limit check never truncates or rounds a value the caller set — that would
27
+ * change what is billed. The offending event is dropped instead (reason
28
+ * `'invalid'`), naming the field, the limit, and the offending value.
29
+ *
30
+ * The one exception is a label the SDK itself reads off an incoming request
31
+ * (the middlewares' `endpointPath` and `httpMethod`): dropping there would let
32
+ * an API consumer avoid metering by sending an over-long path. Those are cut to
33
+ * the limit where they are derived (`truncateRequestLabel`) and the event is
34
+ * still sent.
27
35
  */
28
36
  /** Maximum characters the ingestor accepts per string field (`@Size(max = ...)`). */
29
37
  export const MAX_LENGTHS = {
@@ -36,8 +44,16 @@ export const MAX_LENGTHS = {
36
44
  sessionId: 64,
37
45
  agentId: 36,
38
46
  toolName: 64,
47
+ capabilityName: 64,
39
48
  endpointPath: 512,
40
49
  httpMethod: 16,
50
+ subscriptionId: 64,
51
+ grpcService: 255,
52
+ grpcMethod: 128,
53
+ gqlOperationName: 255,
54
+ wsConnectionId: 64,
55
+ mqttTopic: 500,
56
+ mqttClientId: 128,
41
57
  };
42
58
  /** `@Digits(integer = 14, fraction = 6)` — `usage_events.quantity` is NUMERIC(20,6). */
43
59
  export const MAX_QUANTITY_INTEGER_DIGITS = 14;
@@ -56,6 +72,42 @@ const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?([Z
56
72
  function lengthOf(value) {
57
73
  return value.length;
58
74
  }
75
+ /**
76
+ * Cut `value` to at most `max` UTF-16 code units — how the ingestor counts —
77
+ * without leaving half a surrogate pair at the end.
78
+ */
79
+ export function truncateToLimit(value, max) {
80
+ if (value.length <= max)
81
+ return value;
82
+ let end = Math.max(0, max);
83
+ const last = end > 0 ? value.charCodeAt(end - 1) : 0;
84
+ // A high surrogate at the cut means its low half was cut off: drop it too.
85
+ if (last >= 0xd800 && last <= 0xdbff)
86
+ end -= 1;
87
+ return value.slice(0, end);
88
+ }
89
+ /** Labels already reported as truncated — one WARN per label name per process. */
90
+ const truncationWarned = new Set();
91
+ /**
92
+ * Bound a label the SDK derived from an incoming request to the ingestor's
93
+ * limit for `field`. Logs one WARN per field per process. Not for values the
94
+ * SDK caller sets: those are never altered (an over-long one drops the event).
95
+ */
96
+ export function truncateRequestLabel(field, value) {
97
+ const max = MAX_LENGTHS[field];
98
+ if (typeof value !== 'string' || value.length <= max)
99
+ return value;
100
+ if (!truncationWarned.has(field)) {
101
+ truncationWarned.add(field);
102
+ console.warn(`[aforo] ${field} taken from the request was longer than the ingestor's limit and was `
103
+ + `truncated to ${max} characters; the event is still sent. Logged once per label.`);
104
+ }
105
+ return truncateToLimit(value, max);
106
+ }
107
+ /** Test hook: forget which labels were already reported as truncated. */
108
+ export function resetTruncationWarnings() {
109
+ truncationWarned.clear();
110
+ }
59
111
  function lengthError(field, value) {
60
112
  if (value === undefined || value === null)
61
113
  return null;
@@ -65,7 +117,7 @@ function lengthError(field, value) {
65
117
  if (len <= max)
66
118
  return null;
67
119
  return `${field} is ${len} characters, exceeding the ingestor's ${max}-character limit `
68
- + `(value starts "${text.slice(0, 32)}"). Shorten it — the SDK will not truncate it, `
120
+ + `(value starts "${text.slice(0, 80)}"). Shorten it — the SDK will not truncate it, `
69
121
  + 'because a truncated id bills the wrong thing.';
70
122
  }
71
123
  /**
@@ -107,18 +159,18 @@ function quantityError(quantity) {
107
159
  }
108
160
  function occurredAtError(occurredAt) {
109
161
  if (!ISO_DATE_TIME.test(occurredAt)) {
110
- return `occurredAt "${occurredAt}" is not an ISO-8601 timestamp `
162
+ return `occurredAt "${String(occurredAt).slice(0, 80)}" is not an ISO-8601 timestamp `
111
163
  + '(expected e.g. "2026-03-01T14:30:00Z").';
112
164
  }
113
165
  if (Number.isNaN(Date.parse(occurredAt))) {
114
- return `occurredAt "${occurredAt}" is not a valid date.`;
166
+ return `occurredAt "${String(occurredAt).slice(0, 80)}" is not a valid date.`;
115
167
  }
116
168
  return null;
117
169
  }
118
170
  /**
119
171
  * Describe the first ingestor constraint this event breaks, or `null` when it
120
- * would be accepted. Callers that must not throw (the framework middlewares)
121
- * can use this to skip an event instead.
172
+ * would be accepted. `AforoClient.track()` uses it to drop an invalid event
173
+ * (reason `'invalid'`) before it is buffered.
122
174
  */
123
175
  export function describeLimitViolation(event) {
124
176
  if (event.idempotencyKey !== undefined && event.idempotencyKey !== null
@@ -1 +1 @@
1
- {"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/limits.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/limits.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,6 @@
1
+ import { truncateRequestLabel } from '../limits.js';
2
+ import { AforoClient } from '../client.js';
3
+ import { normalizePath as heuristicNormalize } from '../path-normalizer.js';
1
4
  /**
2
5
  * Metric recorded for each request when `metricName` is not configured.
3
6
  *
@@ -21,11 +24,17 @@ export function resolveMetricName(options, req, res) {
21
24
  }
22
25
  return options.metricName || DEFAULT_METRIC_NAME;
23
26
  }
24
- /** Longest endpointPath the ingestor accepts. */
25
- const MAX_ENDPOINT_PATH = 512;
26
- /** Request path without its query string, capped to what the ingestor accepts. */
27
+ /**
28
+ * Request path without its query string. Read off the incoming request, so an
29
+ * over-long path is cut to the ingestor's limit (512) instead of dropping the
30
+ * event — otherwise a caller could avoid metering with a long URL.
31
+ */
27
32
  export function endpointPathOf(url) {
28
- return (url.split('?')[0] || '/').slice(0, MAX_ENDPOINT_PATH);
33
+ return truncateRequestLabel('endpointPath', String(url).split('?')[0] || '/');
34
+ }
35
+ /** HTTP method off the incoming request, cut to the ingestor's limit (16). */
36
+ export function httpMethodOf(method) {
37
+ return truncateRequestLabel('httpMethod', String(method));
29
38
  }
30
39
  /** First non-empty string among the candidates, or null. */
31
40
  export function firstNonEmpty(...values) {
@@ -38,4 +47,53 @@ export function firstNonEmpty(...values) {
38
47
  }
39
48
  return null;
40
49
  }
50
+ /** The client a middleware meters through: the app's own, or one built from the options. */
51
+ export function middlewareClient(options) {
52
+ if (options?.client)
53
+ return options.client;
54
+ if (!options?.apiKey) {
55
+ throw new Error('Aforo middleware needs either `client` (an AforoClient your app owns) or `apiKey`.');
56
+ }
57
+ return new AforoClient({ ...(options.clientOptions ?? {}), apiKey: options.apiKey, baseUrl: options.baseUrl });
58
+ }
59
+ /**
60
+ * Build the per-request customer lookup. Throws at setup when neither a
61
+ * resolver nor the explicit `trustCustomerHeader` opt-in is configured: the
62
+ * X-Customer-Id header is set by whoever calls the API, so it is never read
63
+ * by default.
64
+ */
65
+ export function customerLookup(options, readHeader) {
66
+ const legacy = options?.customerId;
67
+ const resolver = options?.customerResolver ??
68
+ (typeof legacy === 'function' ? legacy : typeof legacy === 'string' && legacy.trim() ? () => legacy : undefined);
69
+ const trustHeader = options?.trustCustomerHeader === true;
70
+ if (!resolver && !trustHeader) {
71
+ throw new Error('Aforo middleware needs a customerResolver: (req) => the Aforo customer id from identity your app ' +
72
+ 'authenticated (session, verified JWT subject, API-key lookup). To bill the X-Customer-Id header ' +
73
+ 'instead — only safe behind a proxy that sets it — pass trustCustomerHeader: true.');
74
+ }
75
+ return (req, headerSource = req) => {
76
+ const fromResolver = resolver ? firstNonEmpty(resolver(req)) : null;
77
+ if (fromResolver)
78
+ return fromResolver;
79
+ return trustHeader ? firstNonEmpty(readHeader(headerSource)) : null;
80
+ };
81
+ }
82
+ /**
83
+ * The endpointPath to send: the query string removed, dynamic segments
84
+ * collapsed unless `normalizePath` is false (a framework route template wins),
85
+ * and cut to the ingestor's 512-character limit.
86
+ */
87
+ export function endpointPathFor(options, rawPath, req, routeTemplate) {
88
+ const path = String(rawPath ?? '/').split('?')[0] || '/';
89
+ const mode = options?.normalizePath ?? true;
90
+ let out = path;
91
+ if (typeof mode === 'function') {
92
+ out = String(mode(path, req) || path);
93
+ }
94
+ else if (mode) {
95
+ out = heuristicNormalize(path, typeof routeTemplate === 'string' && routeTemplate ? routeTemplate : undefined);
96
+ }
97
+ return endpointPathOf(out);
98
+ }
41
99
  //# sourceMappingURL=common.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"common.js","sourceRoot":"","sources":["../../../src/middleware/common.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,WAAW,CAAC;AAE/C;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,MAA0B;IACpD,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,SAAS,CAAC;AACpD,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,iBAAiB,CAAC,OAA0B,EAAE,GAAQ,EAAE,GAAQ;IAC9E,IAAI,OAAO,OAAO,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC7C,OAAO,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,mBAAmB,CAAC;IAC7D,CAAC;IACD,OAAO,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAC;AACnD,CAAC;AAED,iDAAiD;AACjD,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAE9B,kFAAkF;AAClF,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC;AAChE,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,aAAa,CAAC,GAAG,MAAiB;IAChD,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI;YAAE,SAAS;QAC5C,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACrD,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../../../src/middleware/common.ts"],"names":[],"mappings":"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"}
@@ -1,99 +1,62 @@
1
- import { AforoClient } from '../client';
2
- import { isPreflight, resolveMetricName, firstNonEmpty, endpointPathOf } from './common';
1
+ import { isPreflight, resolveMetricName, httpMethodOf, middlewareClient, customerLookup, endpointPathFor } from './common.js';
3
2
  const DEFAULT_EXCLUDE_PATHS = ['/health', '/ready', '/metrics', '/favicon.ico'];
4
3
  /**
5
- * Express middleware that automatically captures API usage events.
4
+ * Express middleware that records one usage event per request.
6
5
  *
7
- * Hooks into `res.on('finish')` — runs AFTER the response is sent to the client.
8
- * Zero latency impact on the API call itself.
6
+ * Hooks into `res.on('finish')` — runs AFTER the response is sent, so it adds
7
+ * no latency to the request. Metering never throws into the request.
9
8
  *
10
9
  * ```typescript
10
+ * import { AforoClient } from '@aforoai/metering';
11
11
  * import { expressMiddleware } from '@aforoai/metering/middleware/express';
12
- * app.use(expressMiddleware({ apiKey: process.env.AFORO_API_KEY, productType: 'API' }));
12
+ * const aforo = new AforoClient({ apiKey: process.env.AFORO_API_KEY!, productType: 'API' });
13
+ * app.use(expressMiddleware({ client: aforo, customerResolver: (req) => req.user?.aforoCustomerId }));
14
+ * process.once('SIGTERM', async () => { await aforo.shutdown(); process.exit(0); });
13
15
  * ```
14
16
  */
15
17
  export function expressMiddleware(options) {
16
- const client = new AforoClient({
17
- apiKey: options.apiKey,
18
- baseUrl: options.baseUrl,
19
- ...options.clientOptions,
20
- });
18
+ const lookupCustomer = customerLookup(options, (req) => req.headers?.['x-customer-id']);
19
+ const client = middlewareClient(options);
21
20
  const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
22
21
  const excludeStatusCodes = options.excludeStatusCodes ?? [];
23
- return function aforoMeteringMiddleware(req, res, next) {
24
- // Capture timing
22
+ const handler = function aforoMeteringMiddleware(req, res, next) {
25
23
  const startTime = Date.now();
26
24
  res.on('finish', () => {
27
25
  try {
28
26
  const path = req.originalUrl || req.url || '/';
29
27
  const method = req.method || 'UNKNOWN';
30
28
  const statusCode = res.statusCode || 0;
31
- // Check exclusions
32
29
  if (isPreflight(method))
33
30
  return;
34
31
  if (excludePaths.some((p) => path.startsWith(p)))
35
32
  return;
36
33
  if (excludeStatusCodes.includes(statusCode))
37
34
  return;
38
- // Resolve metric name (must be a metric in the tenant's catalog)
39
- const metricName = resolveMetricName(options, req, res);
40
- // Resolve quantity
41
- let quantity;
42
- if (typeof options.quantity === 'function') {
43
- quantity = options.quantity(req, res);
44
- }
45
- else {
46
- quantity = options.quantity ?? 1;
47
- }
48
- // Resolve customer ID
49
- let customerId;
50
- if (typeof options.customerId === 'function') {
51
- customerId = options.customerId(req);
52
- }
53
- else if (options.customerId) {
54
- customerId = options.customerId;
55
- }
56
- else {
57
- customerId = extractCustomerId(req);
58
- }
59
- if (!customerId)
60
- return; // Can't meter without a customer
35
+ const quantity = typeof options.quantity === 'function' ? options.quantity(req, res) : options.quantity ?? 1;
61
36
  if (!(quantity > 0))
62
37
  return; // The ingestor rejects quantity <= 0
63
- // Build metadata
64
- let metadata;
65
- if (options.metadata) {
66
- metadata = options.metadata(req, res);
67
- }
38
+ const route = typeof req.route?.path === 'string' ? `${req.baseUrl ?? ''}${req.route.path}` : undefined;
39
+ // No customer → track() drops it as 'invalid' and counts it; the request is unaffected.
68
40
  client.track({
69
- customerId,
70
- metricName,
41
+ customerId: lookupCustomer(req) ?? '',
42
+ metricName: resolveMetricName(options, req, res),
71
43
  quantity,
72
- metadata,
44
+ metadata: options.metadata ? options.metadata(req, res) : undefined,
73
45
  productType: options.productType,
74
- endpointPath: endpointPathOf(path),
75
- httpMethod: method,
46
+ endpointPath: endpointPathFor(options, path, req, route),
47
+ httpMethod: httpMethodOf(method),
76
48
  statusCode,
77
49
  responseTimeMs: Date.now() - startTime,
78
- }).catch(() => { }); // Fire-and-forget
50
+ }).catch((err) => console.debug(`[aforo] metering skipped: ${err?.message ?? err}`));
79
51
  }
80
- catch {
81
- // Never let metering errors affect the API
52
+ catch (err) {
53
+ // Never let metering errors affect the API.
54
+ console.debug(`[aforo] metering skipped: ${err?.message ?? err}`);
82
55
  }
83
56
  });
84
57
  next();
85
58
  };
86
- }
87
- /**
88
- * Default customer resolution: authenticated user id, then X-Customer-Id.
89
- *
90
- * The caller's X-Api-Key header is deliberately NOT used: that is the end
91
- * user's secret, and using it as customerId wrote credentials into billing data
92
- * while never matching an Aforo customer. Configure `customerId` to map your
93
- * callers to Aforo customer ids.
94
- */
95
- function extractCustomerId(req) {
96
- return firstNonEmpty(req.user?.id, req.user?.sub, req.headers?.['x-customer-id']);
59
+ return Object.assign(handler, { client });
97
60
  }
98
61
  // Also export as `middleware` for convenience
99
62
  export const middleware = expressMiddleware;
@@ -1 +1 @@
1
- {"version":3,"file":"express.js","sourceRoot":"","sources":["../../../src/middleware/express.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../../src/middleware/express.ts"],"names":[],"mappings":"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"}
@@ -1,22 +1,23 @@
1
- import { AforoClient } from '../client';
2
- import { isPreflight, resolveMetricName, firstNonEmpty, endpointPathOf } from './common';
1
+ import { isPreflight, resolveMetricName, httpMethodOf, middlewareClient, customerLookup, endpointPathFor } from './common.js';
3
2
  const DEFAULT_EXCLUDE_PATHS = ['/health', '/ready', '/metrics', '/favicon.ico'];
4
3
  /**
5
- * Fastify plugin that automatically captures API usage events.
6
- *
7
- * Uses the `onResponse` hook — runs AFTER the response is sent.
4
+ * Fastify plugin that records one usage event per request in the
5
+ * `onResponse` hook (after the response is sent).
8
6
  *
9
7
  * ```typescript
8
+ * import { AforoClient } from '@aforoai/metering';
10
9
  * import { fastifyPlugin } from '@aforoai/metering/middleware/fastify';
11
- * fastify.register(fastifyPlugin, { apiKey: process.env.AFORO_API_KEY, productType: 'API' });
10
+ * const aforo = new AforoClient({ apiKey: process.env.AFORO_API_KEY!, productType: 'API' });
11
+ * fastify.register(fastifyPlugin, { client: aforo, customerResolver: (request) => request.user?.aforoCustomerId });
12
12
  * ```
13
+ *
14
+ * When the plugin creates its own client (apiKey instead of client), it shuts
15
+ * that client down in Fastify's onClose hook. A client you pass in is yours to shut down.
13
16
  */
14
17
  export async function fastifyPlugin(fastify, options) {
15
- const client = new AforoClient({
16
- apiKey: options.apiKey,
17
- baseUrl: options.baseUrl,
18
- ...options.clientOptions,
19
- });
18
+ const lookupCustomer = customerLookup(options, (request) => request.headers?.['x-customer-id']);
19
+ const owned = !options.client;
20
+ const client = middlewareClient(options);
20
21
  const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
21
22
  const excludeStatusCodes = options.excludeStatusCodes ?? [];
22
23
  fastify.addHook('onResponse', (request, reply, done) => {
@@ -30,56 +31,35 @@ export async function fastifyPlugin(fastify, options) {
30
31
  return done();
31
32
  if (excludeStatusCodes.includes(statusCode))
32
33
  return done();
33
- const metricName = resolveMetricName(options, request, reply);
34
- let quantity;
35
- if (typeof options.quantity === 'function') {
36
- quantity = options.quantity(request, reply);
37
- }
38
- else {
39
- quantity = options.quantity ?? 1;
40
- }
41
- let customerId;
42
- if (typeof options.customerId === 'function') {
43
- customerId = options.customerId(request);
44
- }
45
- else if (options.customerId) {
46
- customerId = options.customerId;
47
- }
48
- else {
49
- // Never the caller's X-Api-Key: that is a secret, not a customer id.
50
- customerId = firstNonEmpty(request.user?.id, request.headers?.['x-customer-id']);
51
- }
52
- if (!customerId)
53
- return done();
34
+ const quantity = typeof options.quantity === 'function' ? options.quantity(request, reply) : options.quantity ?? 1;
54
35
  if (!(quantity > 0))
55
36
  return done(); // The ingestor rejects quantity <= 0
56
- let metadata;
57
- if (options.metadata) {
58
- metadata = options.metadata(request, reply);
59
- }
60
37
  const elapsed = typeof reply.elapsedTime === 'number'
61
38
  ? reply.elapsedTime
62
39
  : typeof reply.getResponseTime === 'function' ? reply.getResponseTime() : undefined;
63
40
  client.track({
64
- customerId,
65
- metricName,
41
+ customerId: lookupCustomer(request) ?? '',
42
+ metricName: resolveMetricName(options, request, reply),
66
43
  quantity,
67
- metadata,
44
+ metadata: options.metadata ? options.metadata(request, reply) : undefined,
68
45
  productType: options.productType,
69
- endpointPath: endpointPathOf(path),
70
- httpMethod: method,
46
+ endpointPath: endpointPathFor(options, path, request, request.routeOptions?.url ?? request.routerPath),
47
+ httpMethod: httpMethodOf(method),
71
48
  statusCode,
72
49
  ...(typeof elapsed === 'number' ? { responseTimeMs: Math.round(elapsed) } : {}),
73
- }).catch(() => { });
50
+ }).catch((err) => console.debug(`[aforo] metering skipped: ${err?.message ?? err}`));
74
51
  }
75
- catch {
76
- // Never let metering affect the API
52
+ catch (err) {
53
+ // Never let metering affect the API.
54
+ console.debug(`[aforo] metering skipped: ${err?.message ?? err}`);
77
55
  }
78
56
  done();
79
57
  });
80
- fastify.addHook('onClose', async () => {
81
- await client.shutdown();
82
- });
58
+ if (owned) {
59
+ fastify.addHook('onClose', async () => {
60
+ await client.shutdown();
61
+ });
62
+ }
83
63
  }
84
64
  // Mark as fastify plugin
85
65
  fastifyPlugin[Symbol.for('skip-override')] = true;
@@ -1 +1 @@
1
- {"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../../src/middleware/fastify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAEzF,MAAM,qBAAqB,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;AAEhF;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAY,EAAE,OAA0B;IAC1E,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC;QAC7B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,GAAG,OAAO,CAAC,aAAa;KACzB,CAAC,CAAC;IAEH,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,qBAAqB,CAAC;IACnE,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC;IAE5D,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,OAAY,EAAE,KAAU,EAAE,IAAS,EAAE,EAAE;QACpE,IAAI,CAAC;YACH,MAAM,IAAI,GAAW,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC;YACxC,MAAM,MAAM,GAAW,OAAO,CAAC,MAAM,IAAI,SAAS,CAAC;YACnD,MAAM,UAAU,GAAW,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC;YAEjD,IAAI,WAAW,CAAC,MAAM,CAAC;gBAAE,OAAO,IAAI,EAAE,CAAC;YACvC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO,IAAI,EAAE,CAAC;YACxE,IAAI,kBAAkB,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAAE,OAAO,IAAI,EAAE,CAAC;YAE3D,MAAM,UAAU,GAAG,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;YAE9D,IAAI,QAAgB,CAAC;YACrB,IAAI,OAAO,OAAO,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;gBAC3C,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAC9C,CAAC;iBAAM,CAAC;gBACN,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC;YACnC,CAAC;YAED,IAAI,UAAyB,CAAC;YAC9B,IAAI,OAAO,OAAO,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;gBAC7C,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAC3C,CAAC;iBAAM,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;gBAC9B,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;YAClC,CAAC;iBAAM,CAAC;gBACN,qEAAqE;gBACrE,UAAU,GAAG,aAAa,CAAE,OAAe,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;YAC5F,CAAC;YAED,IAAI,CAAC,UAAU;gBAAE,OAAO,IAAI,EAAE,CAAC;YAC/B,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;gBAAE,OAAO,IAAI,EAAE,CAAC,CAAC,qCAAqC;YAEzE,IAAI,QAA+D,CAAC;YACpE,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;gBACrB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAC9C,CAAC;YAED,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ;gBACnD,CAAC,CAAC,KAAK,CAAC,WAAW;gBACnB,CAAC,CAAC,OAAO,KAAK,CAAC,eAAe,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YAEtF,MAAM,CAAC,KAAK,CAAC;gBACX,UAAU;gBACV,UAAU;gBACV,QAAQ;gBACR,QAAQ;gBACR,WAAW,EAAE,OAAO,CAAC,WAAW;gBAChC,YAAY,EAAE,cAAc,CAAC,IAAI,CAAC;gBAClC,UAAU,EAAE,MAAM;gBAClB,UAAU;gBACV,GAAG,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChF,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,oCAAoC;QACtC,CAAC;QAED,IAAI,EAAE,CAAC;IACT,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;QACpC,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,yBAAyB;AACxB,aAAqB,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,GAAG,IAAI,CAAC;AAC1D,aAAqB,CAAC,MAAM,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,GAAG,gBAAgB,CAAC"}
1
+ {"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../../src/middleware/fastify.ts"],"names":[],"mappings":"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"}
@@ -1,25 +1,29 @@
1
- import { AforoClient } from '../client';
2
- import { isPreflight, resolveMetricName, firstNonEmpty, endpointPathOf } from './common';
1
+ import { isPreflight, resolveMetricName, httpMethodOf, middlewareClient, customerLookup, endpointPathFor } from './common.js';
3
2
  const DEFAULT_EXCLUDE_PATHS = ['/health', '/ready', '/metrics', '/favicon.ico'];
4
3
  /**
5
- * Koa middleware that automatically captures API usage events.
6
- *
7
- * Captures after `await next()` completes — runs after the response is generated.
4
+ * Koa middleware that records one usage event per request, after `await next()`.
5
+ * Metering never throws into the request.
8
6
  *
9
7
  * ```typescript
8
+ * import { AforoClient } from '@aforoai/metering';
10
9
  * import { koaMiddleware } from '@aforoai/metering/middleware/koa';
11
- * app.use(koaMiddleware({ apiKey: process.env.AFORO_API_KEY, productType: 'API' }));
10
+ * const aforo = new AforoClient({ apiKey: process.env.AFORO_API_KEY!, productType: 'API' });
11
+ * app.use(koaMiddleware({ client: aforo, customerResolver: (ctx) => ctx.state.user?.aforoCustomerId }));
12
12
  * ```
13
+ *
14
+ * `customerResolver` receives the Koa context.
13
15
  */
14
16
  export function koaMiddleware(options) {
15
- const client = new AforoClient({
16
- apiKey: options.apiKey,
17
- baseUrl: options.baseUrl,
18
- ...options.clientOptions,
19
- });
17
+ // customerResolver receives the Koa context; the deprecated customerId function kept receiving ctx.request.
18
+ const legacy = options.customerId;
19
+ const lookupCustomer = customerLookup({
20
+ ...options,
21
+ customerResolver: options.customerResolver ?? (typeof legacy === 'function' ? (ctx) => legacy(ctx?.request) : undefined),
22
+ }, (ctx) => ctx?.get?.('x-customer-id') || ctx?.request?.headers?.['x-customer-id']);
23
+ const client = middlewareClient(options);
20
24
  const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
21
25
  const excludeStatusCodes = options.excludeStatusCodes ?? [];
22
- return async function aforoMeteringMiddleware(ctx, next) {
26
+ const handler = async function aforoMeteringMiddleware(ctx, next) {
23
27
  const startTime = Date.now();
24
28
  await next();
25
29
  try {
@@ -32,48 +36,26 @@ export function koaMiddleware(options) {
32
36
  return;
33
37
  if (excludeStatusCodes.includes(statusCode))
34
38
  return;
35
- const metricName = resolveMetricName(options, ctx.request, ctx.response);
36
- let quantity;
37
- if (typeof options.quantity === 'function') {
38
- quantity = options.quantity(ctx.request, ctx.response);
39
- }
40
- else {
41
- quantity = options.quantity ?? 1;
42
- }
43
- let customerId;
44
- if (typeof options.customerId === 'function') {
45
- customerId = options.customerId(ctx.request);
46
- }
47
- else if (options.customerId) {
48
- customerId = options.customerId;
49
- }
50
- else {
51
- // Never the caller's X-Api-Key: that is a secret, not a customer id.
52
- customerId = firstNonEmpty(ctx.state?.user?.id, ctx.get('x-customer-id'));
53
- }
54
- if (!customerId)
55
- return;
39
+ const quantity = typeof options.quantity === 'function' ? options.quantity(ctx.request, ctx.response) : options.quantity ?? 1;
56
40
  if (!(quantity > 0))
57
41
  return; // The ingestor rejects quantity <= 0
58
- let metadata;
59
- if (options.metadata) {
60
- metadata = options.metadata(ctx.request, ctx.response);
61
- }
62
42
  client.track({
63
- customerId,
64
- metricName,
43
+ customerId: lookupCustomer(ctx) ?? '',
44
+ metricName: resolveMetricName(options, ctx.request, ctx.response),
65
45
  quantity,
66
- metadata,
46
+ metadata: options.metadata ? options.metadata(ctx.request, ctx.response) : undefined,
67
47
  productType: options.productType,
68
- endpointPath: endpointPathOf(path),
69
- httpMethod: method,
48
+ endpointPath: endpointPathFor(options, path, ctx, ctx._matchedRoute ?? ctx.routerPath),
49
+ httpMethod: httpMethodOf(method),
70
50
  statusCode,
71
51
  responseTimeMs: Date.now() - startTime,
72
- }).catch(() => { });
52
+ }).catch((err) => console.debug(`[aforo] metering skipped: ${err?.message ?? err}`));
73
53
  }
74
- catch {
75
- // Never let metering affect the API
54
+ catch (err) {
55
+ // Never let metering affect the API.
56
+ console.debug(`[aforo] metering skipped: ${err?.message ?? err}`);
76
57
  }
77
58
  };
59
+ return Object.assign(handler, { client });
78
60
  }
79
61
  //# sourceMappingURL=koa.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"koa.js","sourceRoot":"","sources":["../../../src/middleware/koa.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAEzF,MAAM,qBAAqB,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;AAEhF;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,OAA0B;IACtD,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC;QAC7B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,GAAG,OAAO,CAAC,aAAa;KACzB,CAAC,CAAC;IAEH,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,qBAAqB,CAAC;IACnE,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC;IAE5D,OAAO,KAAK,UAAU,uBAAuB,CAAC,GAAQ,EAAE,IAAS;QAC/D,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,IAAI,EAAE,CAAC;QAEb,IAAI,CAAC;YACH,MAAM,IAAI,GAAW,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC;YAChD,MAAM,MAAM,GAAW,GAAG,CAAC,MAAM,IAAI,SAAS,CAAC;YAC/C,MAAM,UAAU,GAAW,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;YAE3C,IAAI,WAAW,CAAC,MAAM,CAAC;gBAAE,OAAO;YAChC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO;YACjE,IAAI,kBAAkB,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAAE,OAAO;YAEpD,MAAM,UAAU,GAAG,iBAAiB,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;YAEzE,IAAI,QAAgB,CAAC;YACrB,IAAI,OAAO,OAAO,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;gBAC3C,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;YACzD,CAAC;iBAAM,CAAC;gBACN,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC;YACnC,CAAC;YAED,IAAI,UAAyB,CAAC;YAC9B,IAAI,OAAO,OAAO,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;gBAC7C,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC/C,CAAC;iBAAM,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;gBAC9B,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;YAClC,CAAC;iBAAM,CAAC;gBACN,qEAAqE;gBACrE,UAAU,GAAG,aAAa,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC;YAC5E,CAAC;YAED,IAAI,CAAC,UAAU;gBAAE,OAAO;YACxB,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;gBAAE,OAAO,CAAC,qCAAqC;YAElE,IAAI,QAA+D,CAAC;YACpE,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;gBACrB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;YACzD,CAAC;YAED,MAAM,CAAC,KAAK,CAAC;gBACX,UAAU;gBACV,UAAU;gBACV,QAAQ;gBACR,QAAQ;gBACR,WAAW,EAAE,OAAO,CAAC,WAAW;gBAChC,YAAY,EAAE,cAAc,CAAC,IAAI,CAAC;gBAClC,UAAU,EAAE,MAAM;gBAClB,UAAU;gBACV,cAAc,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;aACvC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,oCAAoC;QACtC,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"koa.js","sourceRoot":"","sources":["../../../src/middleware/koa.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9H,MAAM,qBAAqB,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;AAEhF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,OAA0B;IACtD,4GAA4G;IAC5G,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAClC,MAAM,cAAc,GAAG,cAAc,CACnC;QACE,GAAG,OAAO;QACV,gBAAgB,EACd,OAAO,CAAC,gBAAgB,IAAI,CAAC,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;KAC9G,EACD,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,IAAI,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,eAAe,CAAC,CACjF,CAAC;IACF,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAEzC,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,qBAAqB,CAAC;IACnE,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC;IAE5D,MAAM,OAAO,GAAG,KAAK,UAAU,uBAAuB,CAAC,GAAQ,EAAE,IAAS;QACxE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,IAAI,EAAE,CAAC;QAEb,IAAI,CAAC;YACH,MAAM,IAAI,GAAW,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC;YAChD,MAAM,MAAM,GAAW,GAAG,CAAC,MAAM,IAAI,SAAS,CAAC;YAC/C,MAAM,UAAU,GAAW,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;YAE3C,IAAI,WAAW,CAAC,MAAM,CAAC;gBAAE,OAAO;YAChC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO;YACjE,IAAI,kBAAkB,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAAE,OAAO;YAEpD,MAAM,QAAQ,GACZ,OAAO,OAAO,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC;YAC/G,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;gBAAE,OAAO,CAAC,qCAAqC;YAElE,MAAM,CAAC,KAAK,CAAC;gBACX,UAAU,EAAE,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE;gBACrC,UAAU,EAAE,iBAAiB,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC;gBACjE,QAAQ;gBACR,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS;gBACpF,WAAW,EAAE,OAAO,CAAC,WAAW;gBAChC,YAAY,EAAE,eAAe,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC,aAAa,IAAI,GAAG,CAAC,UAAU,CAAC;gBACtF,UAAU,EAAE,YAAY,CAAC,MAAM,CAAC;gBAChC,UAAU;gBACV,cAAc,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;aACvC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,6BAA6B,GAAG,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;QACvF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,qCAAqC;YACrC,OAAO,CAAC,KAAK,CAAC,6BAA8B,GAAa,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC,CAAC;IACF,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AAC5C,CAAC"}