@aforoai/metering 1.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 (98) hide show
  1. package/README.md +88 -0
  2. package/dist/cjs/buffer.d.ts +27 -0
  3. package/dist/cjs/buffer.d.ts.map +1 -0
  4. package/dist/cjs/buffer.js +75 -0
  5. package/dist/cjs/buffer.js.map +1 -0
  6. package/dist/cjs/client.d.ts +86 -0
  7. package/dist/cjs/client.d.ts.map +1 -0
  8. package/dist/cjs/client.js +301 -0
  9. package/dist/cjs/client.js.map +1 -0
  10. package/dist/cjs/idempotency.d.ts +21 -0
  11. package/dist/cjs/idempotency.d.ts.map +1 -0
  12. package/dist/cjs/idempotency.js +31 -0
  13. package/dist/cjs/idempotency.js.map +1 -0
  14. package/dist/cjs/index.d.ts +9 -0
  15. package/dist/cjs/index.d.ts.map +1 -0
  16. package/dist/cjs/index.js +23 -0
  17. package/dist/cjs/index.js.map +1 -0
  18. package/dist/cjs/limits.d.ts +80 -0
  19. package/dist/cjs/limits.d.ts.map +1 -0
  20. package/dist/cjs/limits.js +143 -0
  21. package/dist/cjs/limits.js.map +1 -0
  22. package/dist/cjs/middleware/common.d.ts +22 -0
  23. package/dist/cjs/middleware/common.d.ts.map +1 -0
  24. package/dist/cjs/middleware/common.js +48 -0
  25. package/dist/cjs/middleware/common.js.map +1 -0
  26. package/dist/cjs/middleware/express.d.ts +15 -0
  27. package/dist/cjs/middleware/express.d.ts.map +1 -0
  28. package/dist/cjs/middleware/express.js +104 -0
  29. package/dist/cjs/middleware/express.js.map +1 -0
  30. package/dist/cjs/middleware/fastify.d.ts +13 -0
  31. package/dist/cjs/middleware/fastify.d.ts.map +1 -0
  32. package/dist/cjs/middleware/fastify.js +90 -0
  33. package/dist/cjs/middleware/fastify.js.map +1 -0
  34. package/dist/cjs/middleware/koa.d.ts +13 -0
  35. package/dist/cjs/middleware/koa.d.ts.map +1 -0
  36. package/dist/cjs/middleware/koa.js +82 -0
  37. package/dist/cjs/middleware/koa.js.map +1 -0
  38. package/dist/cjs/path-normalizer.d.ts +15 -0
  39. package/dist/cjs/path-normalizer.d.ts.map +1 -0
  40. package/dist/cjs/path-normalizer.js +49 -0
  41. package/dist/cjs/path-normalizer.js.map +1 -0
  42. package/dist/cjs/transport.d.ts +39 -0
  43. package/dist/cjs/transport.d.ts.map +1 -0
  44. package/dist/cjs/transport.js +116 -0
  45. package/dist/cjs/transport.js.map +1 -0
  46. package/dist/cjs/types.d.ts +128 -0
  47. package/dist/cjs/types.d.ts.map +1 -0
  48. package/dist/cjs/types.js +3 -0
  49. package/dist/cjs/types.js.map +1 -0
  50. package/dist/esm/buffer.js +71 -0
  51. package/dist/esm/buffer.js.map +1 -0
  52. package/dist/esm/client.js +297 -0
  53. package/dist/esm/client.js.map +1 -0
  54. package/dist/esm/idempotency.js +27 -0
  55. package/dist/esm/idempotency.js.map +1 -0
  56. package/dist/esm/index.js +9 -0
  57. package/dist/esm/index.js.map +1 -0
  58. package/dist/esm/limits.js +138 -0
  59. package/dist/esm/limits.js.map +1 -0
  60. package/dist/esm/middleware/common.js +41 -0
  61. package/dist/esm/middleware/common.js.map +1 -0
  62. package/dist/esm/middleware/express.js +100 -0
  63. package/dist/esm/middleware/express.js.map +1 -0
  64. package/dist/esm/middleware/fastify.js +87 -0
  65. package/dist/esm/middleware/fastify.js.map +1 -0
  66. package/dist/esm/middleware/koa.js +79 -0
  67. package/dist/esm/middleware/koa.js.map +1 -0
  68. package/dist/esm/path-normalizer.js +46 -0
  69. package/dist/esm/path-normalizer.js.map +1 -0
  70. package/dist/esm/transport.js +112 -0
  71. package/dist/esm/transport.js.map +1 -0
  72. package/dist/esm/types.js +2 -0
  73. package/dist/esm/types.js.map +1 -0
  74. package/dist/types/buffer.d.ts +27 -0
  75. package/dist/types/buffer.d.ts.map +1 -0
  76. package/dist/types/client.d.ts +86 -0
  77. package/dist/types/client.d.ts.map +1 -0
  78. package/dist/types/idempotency.d.ts +21 -0
  79. package/dist/types/idempotency.d.ts.map +1 -0
  80. package/dist/types/index.d.ts +9 -0
  81. package/dist/types/index.d.ts.map +1 -0
  82. package/dist/types/limits.d.ts +80 -0
  83. package/dist/types/limits.d.ts.map +1 -0
  84. package/dist/types/middleware/common.d.ts +22 -0
  85. package/dist/types/middleware/common.d.ts.map +1 -0
  86. package/dist/types/middleware/express.d.ts +15 -0
  87. package/dist/types/middleware/express.d.ts.map +1 -0
  88. package/dist/types/middleware/fastify.d.ts +13 -0
  89. package/dist/types/middleware/fastify.d.ts.map +1 -0
  90. package/dist/types/middleware/koa.d.ts +13 -0
  91. package/dist/types/middleware/koa.d.ts.map +1 -0
  92. package/dist/types/path-normalizer.d.ts +15 -0
  93. package/dist/types/path-normalizer.d.ts.map +1 -0
  94. package/dist/types/transport.d.ts +39 -0
  95. package/dist/types/transport.d.ts.map +1 -0
  96. package/dist/types/types.d.ts +128 -0
  97. package/dist/types/types.d.ts.map +1 -0
  98. package/package.json +74 -0
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateIdempotencyKey = generateIdempotencyKey;
4
+ exports.generateRandomKey = generateRandomKey;
5
+ const node_crypto_1 = require("node:crypto");
6
+ /**
7
+ * Generate a deterministic idempotency key from event fields.
8
+ * Uses SHA-256, truncated to 32 hex chars for compact storage.
9
+ *
10
+ * Formula: SHA256(customerId + metricName + quantity + occurredAt)
11
+ *
12
+ * NOT the client default any more. `occurredAt` only carries millisecond
13
+ * precision, so two genuinely distinct events for the same customer + metric
14
+ * + quantity inside one millisecond hash to the same key and the ingestor
15
+ * drops the second as a DUPLICATE — silent under-billing. Kept exported for
16
+ * callers who deliberately want content-addressed dedup (e.g. replaying a
17
+ * fixed batch) and pass the result to `track({ idempotencyKey })` themselves.
18
+ */
19
+ function generateIdempotencyKey(customerId, metricName, quantity, occurredAt) {
20
+ const input = `${customerId}:${metricName}:${quantity}:${occurredAt}`;
21
+ return (0, node_crypto_1.createHash)('sha256').update(input).digest('hex').substring(0, 32);
22
+ }
23
+ /**
24
+ * Generate a random UUID v4. This is the default key for an event whose caller
25
+ * supplied none: every event gets its own key, so no two distinct events can
26
+ * collide. Dedup stays opt-in via an explicit `idempotencyKey`.
27
+ */
28
+ function generateRandomKey() {
29
+ return (0, node_crypto_1.randomUUID)();
30
+ }
31
+ //# sourceMappingURL=idempotency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../../src/idempotency.ts"],"names":[],"mappings":";;AAeA,wDAQC;AAOD,8CAEC;AAhCD,6CAAqD;AAErD;;;;;;;;;;;;GAYG;AACH,SAAgB,sBAAsB,CACpC,UAAkB,EAClB,UAAkB,EAClB,QAAgB,EAChB,UAAkB;IAElB,MAAM,KAAK,GAAG,GAAG,UAAU,IAAI,UAAU,IAAI,QAAQ,IAAI,UAAU,EAAE,CAAC;IACtE,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC3E,CAAC;AAED;;;;GAIG;AACH,SAAgB,iBAAiB;IAC/B,OAAO,IAAA,wBAAU,GAAE,CAAC;AACtB,CAAC"}
@@ -0,0 +1,9 @@
1
+ export { AforoClient } from './client';
2
+ export { expressMiddleware, middleware } from './middleware/express';
3
+ export { fastifyPlugin } from './middleware/fastify';
4
+ export { koaMiddleware } from './middleware/koa';
5
+ export { normalizePath } from './path-normalizer';
6
+ export { DEFAULT_METRIC_NAME } from './middleware/common';
7
+ export { MAX_LENGTHS, MAX_QUANTITY_DECIMAL_PLACES, MAX_QUANTITY_INTEGER_DIGITS, describeLimitViolation, } from './limits';
8
+ export type { AforoOptions, TrackEvent, MiddlewareOptions, FlushResult, BatchRequest, BatchResponse, } from './types';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EACL,WAAW,EACX,2BAA2B,EAC3B,2BAA2B,EAC3B,sBAAsB,GACvB,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,aAAa,GACd,MAAM,SAAS,CAAC"}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ // Public API for @aforoai/metering
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.describeLimitViolation = exports.MAX_QUANTITY_INTEGER_DIGITS = exports.MAX_QUANTITY_DECIMAL_PLACES = exports.MAX_LENGTHS = exports.DEFAULT_METRIC_NAME = exports.normalizePath = exports.koaMiddleware = exports.fastifyPlugin = exports.middleware = exports.expressMiddleware = exports.AforoClient = void 0;
5
+ var client_1 = require("./client");
6
+ Object.defineProperty(exports, "AforoClient", { enumerable: true, get: function () { return client_1.AforoClient; } });
7
+ var express_1 = require("./middleware/express");
8
+ Object.defineProperty(exports, "expressMiddleware", { enumerable: true, get: function () { return express_1.expressMiddleware; } });
9
+ Object.defineProperty(exports, "middleware", { enumerable: true, get: function () { return express_1.middleware; } });
10
+ var fastify_1 = require("./middleware/fastify");
11
+ Object.defineProperty(exports, "fastifyPlugin", { enumerable: true, get: function () { return fastify_1.fastifyPlugin; } });
12
+ var koa_1 = require("./middleware/koa");
13
+ Object.defineProperty(exports, "koaMiddleware", { enumerable: true, get: function () { return koa_1.koaMiddleware; } });
14
+ var path_normalizer_1 = require("./path-normalizer");
15
+ Object.defineProperty(exports, "normalizePath", { enumerable: true, get: function () { return path_normalizer_1.normalizePath; } });
16
+ var common_1 = require("./middleware/common");
17
+ Object.defineProperty(exports, "DEFAULT_METRIC_NAME", { enumerable: true, get: function () { return common_1.DEFAULT_METRIC_NAME; } });
18
+ var limits_1 = require("./limits");
19
+ Object.defineProperty(exports, "MAX_LENGTHS", { enumerable: true, get: function () { return limits_1.MAX_LENGTHS; } });
20
+ Object.defineProperty(exports, "MAX_QUANTITY_DECIMAL_PLACES", { enumerable: true, get: function () { return limits_1.MAX_QUANTITY_DECIMAL_PLACES; } });
21
+ Object.defineProperty(exports, "MAX_QUANTITY_INTEGER_DIGITS", { enumerable: true, get: function () { return limits_1.MAX_QUANTITY_INTEGER_DIGITS; } });
22
+ Object.defineProperty(exports, "describeLimitViolation", { enumerable: true, get: function () { return limits_1.describeLimitViolation; } });
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA,mCAAmC;;;AAEnC,mCAAuC;AAA9B,qGAAA,WAAW,OAAA;AACpB,gDAAqE;AAA5D,4GAAA,iBAAiB,OAAA;AAAE,qGAAA,UAAU,OAAA;AACtC,gDAAqD;AAA5C,wGAAA,aAAa,OAAA;AACtB,wCAAiD;AAAxC,oGAAA,aAAa,OAAA;AACtB,qDAAkD;AAAzC,gHAAA,aAAa,OAAA;AACtB,8CAA0D;AAAjD,6GAAA,mBAAmB,OAAA;AAC5B,mCAKkB;AAJhB,qGAAA,WAAW,OAAA;AACX,qHAAA,2BAA2B,OAAA;AAC3B,qHAAA,2BAA2B,OAAA;AAC3B,gHAAA,sBAAsB,OAAA"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Client-side mirror of the ingestor's per-event field constraints.
3
+ *
4
+ * An event that breaks one of these is rejected by the ingestor and never
5
+ * billed. The server reports it per event (`errors[]` in the batch response,
6
+ * indexed), so the rest of the batch survives — but the SDK sends in the
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.
10
+ *
11
+ * Source: `dto/IngestUsageEventRequest` (aforo-nextgen-usage-ingestor-service) —
12
+ * the `@Size` and `@Digits` bean constraints, which are compiled into the
13
+ * server and therefore identical in every environment.
14
+ *
15
+ * Deliberately NOT mirrored here: the timestamp window (`max-age-days`,
16
+ * `future-tolerance-minutes`) and the metadata cap (`max-metadata-bytes`) in
17
+ * `validation/UsageEventValidator`. Each is a per-environment property — a
18
+ * tenant may raise `max-age-days` to 365 for backfills — so enforcing the
19
+ * default here would make the SDK refuse usage its own server would accept and
20
+ * bill. Refusing real usage is a worse failure than the rejection it prevents.
21
+ * Only `occurredAt`'s shape is checked, since a malformed timestamp is invalid
22
+ * under every configuration.
23
+ *
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.
27
+ */
28
+ /** Maximum characters the ingestor accepts per string field (`@Size(max = ...)`). */
29
+ export declare const MAX_LENGTHS: {
30
+ readonly customerId: 64;
31
+ readonly metricName: 255;
32
+ readonly idempotencyKey: 255;
33
+ readonly productType: 20;
34
+ readonly traceId: 128;
35
+ readonly spanId: 32;
36
+ readonly sessionId: 64;
37
+ readonly agentId: 36;
38
+ readonly toolName: 64;
39
+ readonly endpointPath: 512;
40
+ readonly httpMethod: 16;
41
+ };
42
+ /** `@Digits(integer = 14, fraction = 6)` — `usage_events.quantity` is NUMERIC(20,6). */
43
+ export declare const MAX_QUANTITY_INTEGER_DIGITS = 14;
44
+ export declare const MAX_QUANTITY_DECIMAL_PLACES = 6;
45
+ /**
46
+ * Integer and decimal digit counts as the ingestor's `@Digits` sees them, i.e.
47
+ * as `BigDecimal` reads the number the SDK puts on the wire:
48
+ * `integerDigits = precision - scale`, `decimalPlaces = max(scale, 0)`.
49
+ *
50
+ * Works from the serialized text so exponent notation (`1e+21`, which
51
+ * `JSON.stringify` emits for large numbers) is counted the same way the server
52
+ * will count it.
53
+ */
54
+ export declare function quantityDigits(serialized: string): {
55
+ integerDigits: number;
56
+ decimalPlaces: number;
57
+ };
58
+ /** The already-resolved event fields the limits apply to. */
59
+ export interface LimitCheckedEvent {
60
+ customerId?: unknown;
61
+ metricName?: unknown;
62
+ quantity: number;
63
+ idempotencyKey?: unknown;
64
+ occurredAt: string;
65
+ productType?: unknown;
66
+ endpointPath?: unknown;
67
+ httpMethod?: unknown;
68
+ traceId?: unknown;
69
+ spanId?: unknown;
70
+ sessionId?: unknown;
71
+ agentId?: unknown;
72
+ toolName?: unknown;
73
+ }
74
+ /**
75
+ * Describe the first ingestor constraint this event breaks, or `null` when it
76
+ * would be accepted. Callers that must not throw (the framework middlewares)
77
+ * can use this to skip an event instead.
78
+ */
79
+ export declare function describeLimitViolation(event: LimitCheckedEvent): string | null;
80
+ //# sourceMappingURL=limits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,qFAAqF;AACrF,eAAO,MAAM,WAAW;;;;;;;;;;;;CAYd,CAAC;AAEX,wFAAwF;AACxF,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAC9C,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAoC7C;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAYnG;AA+BD,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAe9E"}
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ /**
3
+ * Client-side mirror of the ingestor's per-event field constraints.
4
+ *
5
+ * An event that breaks one of these is rejected by the ingestor and never
6
+ * billed. The server reports it per event (`errors[]` in the batch response,
7
+ * indexed), so the rest of the batch survives — but the SDK sends in the
8
+ * background, so that report reaches nobody: the usage is simply gone. Checking
9
+ * the same limits at `track()` surfaces the problem to the caller, at the call
10
+ * site that produced it, while the event can still be corrected.
11
+ *
12
+ * Source: `dto/IngestUsageEventRequest` (aforo-nextgen-usage-ingestor-service) —
13
+ * the `@Size` and `@Digits` bean constraints, which are compiled into the
14
+ * server and therefore identical in every environment.
15
+ *
16
+ * Deliberately NOT mirrored here: the timestamp window (`max-age-days`,
17
+ * `future-tolerance-minutes`) and the metadata cap (`max-metadata-bytes`) in
18
+ * `validation/UsageEventValidator`. Each is a per-environment property — a
19
+ * tenant may raise `max-age-days` to 365 for backfills — so enforcing the
20
+ * default here would make the SDK refuse usage its own server would accept and
21
+ * bill. Refusing real usage is a worse failure than the rejection it prevents.
22
+ * Only `occurredAt`'s shape is checked, since a malformed timestamp is invalid
23
+ * under every configuration.
24
+ *
25
+ * Nothing here truncates or rounds — that would change what is billed. The
26
+ * offending event is rejected instead, naming the field, the limit, and the
27
+ * offending value.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.MAX_QUANTITY_DECIMAL_PLACES = exports.MAX_QUANTITY_INTEGER_DIGITS = exports.MAX_LENGTHS = void 0;
31
+ exports.quantityDigits = quantityDigits;
32
+ exports.describeLimitViolation = describeLimitViolation;
33
+ /** Maximum characters the ingestor accepts per string field (`@Size(max = ...)`). */
34
+ exports.MAX_LENGTHS = {
35
+ customerId: 64,
36
+ metricName: 255,
37
+ idempotencyKey: 255,
38
+ productType: 20,
39
+ traceId: 128,
40
+ spanId: 32,
41
+ sessionId: 64,
42
+ agentId: 36,
43
+ toolName: 64,
44
+ endpointPath: 512,
45
+ httpMethod: 16,
46
+ };
47
+ /** `@Digits(integer = 14, fraction = 6)` — `usage_events.quantity` is NUMERIC(20,6). */
48
+ exports.MAX_QUANTITY_INTEGER_DIGITS = 14;
49
+ exports.MAX_QUANTITY_DECIMAL_PLACES = 6;
50
+ /**
51
+ * ISO-8601 date-time, the shape Jackson can read into an `Instant`. The zone
52
+ * designator is optional here: the ingestor's example values all carry `Z`, but
53
+ * an offset-less value is not provably rejected, and falsely rejecting one
54
+ * would drop a billable event.
55
+ */
56
+ const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?([Zz]|[+-]\d{2}:?\d{2})?$/;
57
+ /**
58
+ * Length as the ingestor counts it: `String.length()` in Java is UTF-16 code
59
+ * units, which is exactly what `String.prototype.length` returns.
60
+ */
61
+ function lengthOf(value) {
62
+ return value.length;
63
+ }
64
+ function lengthError(field, value) {
65
+ if (value === undefined || value === null)
66
+ return null;
67
+ const text = String(value);
68
+ const max = exports.MAX_LENGTHS[field];
69
+ const len = lengthOf(text);
70
+ if (len <= max)
71
+ return null;
72
+ return `${field} is ${len} characters, exceeding the ingestor's ${max}-character limit `
73
+ + `(value starts "${text.slice(0, 32)}"). Shorten it — the SDK will not truncate it, `
74
+ + 'because a truncated id bills the wrong thing.';
75
+ }
76
+ /**
77
+ * Integer and decimal digit counts as the ingestor's `@Digits` sees them, i.e.
78
+ * as `BigDecimal` reads the number the SDK puts on the wire:
79
+ * `integerDigits = precision - scale`, `decimalPlaces = max(scale, 0)`.
80
+ *
81
+ * Works from the serialized text so exponent notation (`1e+21`, which
82
+ * `JSON.stringify` emits for large numbers) is counted the same way the server
83
+ * will count it.
84
+ */
85
+ function quantityDigits(serialized) {
86
+ const match = /^[+-]?(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(serialized.trim());
87
+ if (!match)
88
+ return { integerDigits: 0, decimalPlaces: 0 };
89
+ const intPart = match[1] ?? '';
90
+ const fracPart = match[2] ?? '';
91
+ const exponent = match[3] ? parseInt(match[3], 10) : 0;
92
+ const digits = (intPart + fracPart).replace(/^0+/, '');
93
+ const precision = digits.length === 0 ? 1 : digits.length;
94
+ const scale = fracPart.length - exponent;
95
+ return { integerDigits: precision - scale, decimalPlaces: Math.max(scale, 0) };
96
+ }
97
+ function quantityError(quantity) {
98
+ // JSON.stringify is what the transport uses, so this is the exact text the
99
+ // ingestor will parse into the BigDecimal that @Digits inspects.
100
+ const serialized = JSON.stringify(quantity);
101
+ const { integerDigits, decimalPlaces } = quantityDigits(String(serialized));
102
+ if (integerDigits > exports.MAX_QUANTITY_INTEGER_DIGITS) {
103
+ return `quantity ${serialized} has ${integerDigits} integer digits, exceeding the ingestor's `
104
+ + `limit of ${exports.MAX_QUANTITY_INTEGER_DIGITS} (usage_events.quantity is NUMERIC(20,6)).`;
105
+ }
106
+ if (decimalPlaces > exports.MAX_QUANTITY_DECIMAL_PLACES) {
107
+ return `quantity ${serialized} has ${decimalPlaces} decimal places, exceeding the ingestor's `
108
+ + `limit of ${exports.MAX_QUANTITY_DECIMAL_PLACES}. Round it yourself before tracking — the SDK will `
109
+ + 'not round it, because that would change the quantity you are billed for.';
110
+ }
111
+ return null;
112
+ }
113
+ function occurredAtError(occurredAt) {
114
+ if (!ISO_DATE_TIME.test(occurredAt)) {
115
+ return `occurredAt "${occurredAt}" is not an ISO-8601 timestamp `
116
+ + '(expected e.g. "2026-03-01T14:30:00Z").';
117
+ }
118
+ if (Number.isNaN(Date.parse(occurredAt))) {
119
+ return `occurredAt "${occurredAt}" is not a valid date.`;
120
+ }
121
+ return null;
122
+ }
123
+ /**
124
+ * Describe the first ingestor constraint this event breaks, or `null` when it
125
+ * would be accepted. Callers that must not throw (the framework middlewares)
126
+ * can use this to skip an event instead.
127
+ */
128
+ function describeLimitViolation(event) {
129
+ if (event.idempotencyKey !== undefined && event.idempotencyKey !== null
130
+ && !String(event.idempotencyKey).trim()) {
131
+ return 'idempotencyKey must not be blank when provided.';
132
+ }
133
+ for (const field of Object.keys(exports.MAX_LENGTHS)) {
134
+ const problem = lengthError(field, event[field]);
135
+ if (problem)
136
+ return problem;
137
+ }
138
+ const quantityProblem = quantityError(event.quantity);
139
+ if (quantityProblem)
140
+ return quantityProblem;
141
+ return occurredAtError(event.occurredAt);
142
+ }
143
+ //# sourceMappingURL=limits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/limits.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { MiddlewareOptions } from '../types';
2
+ /**
3
+ * Metric recorded for each request when `metricName` is not configured.
4
+ *
5
+ * It must exist in the tenant's Aforo metric catalog: the ingestor rejects an
6
+ * unknown metric, and because it validates a batch as a whole, one such event
7
+ * fails the entire batch with 400. The previous default, `"METHOD /path"`, is a
8
+ * name no catalog contains, so every event failed out of the box.
9
+ */
10
+ export declare const DEFAULT_METRIC_NAME = "api_calls";
11
+ /**
12
+ * CORS preflights are a browser protocol detail, not a billable call, and carry
13
+ * no credentials -- so they never have a customer. They are never metered.
14
+ */
15
+ export declare function isPreflight(method: string | undefined): boolean;
16
+ /** Resolve the metric: resolver function, then fixed name, then DEFAULT_METRIC_NAME. */
17
+ export declare function resolveMetricName(options: MiddlewareOptions, req: any, res: any): string;
18
+ /** Request path without its query string, capped to what the ingestor accepts. */
19
+ export declare function endpointPathOf(url: string): string;
20
+ /** First non-empty string among the candidates, or null. */
21
+ export declare function firstNonEmpty(...values: unknown[]): string | null;
22
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/middleware/common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAE/C;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE/D;AAED,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,MAAM,CAKxF;AAKD,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,4DAA4D;AAC5D,wBAAgB,aAAa,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,GAAG,IAAI,CAOjE"}
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_METRIC_NAME = void 0;
4
+ exports.isPreflight = isPreflight;
5
+ exports.resolveMetricName = resolveMetricName;
6
+ exports.endpointPathOf = endpointPathOf;
7
+ exports.firstNonEmpty = firstNonEmpty;
8
+ /**
9
+ * Metric recorded for each request when `metricName` is not configured.
10
+ *
11
+ * It must exist in the tenant's Aforo metric catalog: the ingestor rejects an
12
+ * unknown metric, and because it validates a batch as a whole, one such event
13
+ * fails the entire batch with 400. The previous default, `"METHOD /path"`, is a
14
+ * name no catalog contains, so every event failed out of the box.
15
+ */
16
+ exports.DEFAULT_METRIC_NAME = 'api_calls';
17
+ /**
18
+ * CORS preflights are a browser protocol detail, not a billable call, and carry
19
+ * no credentials -- so they never have a customer. They are never metered.
20
+ */
21
+ function isPreflight(method) {
22
+ return (method || '').toUpperCase() === 'OPTIONS';
23
+ }
24
+ /** Resolve the metric: resolver function, then fixed name, then DEFAULT_METRIC_NAME. */
25
+ function resolveMetricName(options, req, res) {
26
+ if (typeof options.metricName === 'function') {
27
+ return options.metricName(req, res) || exports.DEFAULT_METRIC_NAME;
28
+ }
29
+ return options.metricName || exports.DEFAULT_METRIC_NAME;
30
+ }
31
+ /** Longest endpointPath the ingestor accepts. */
32
+ const MAX_ENDPOINT_PATH = 512;
33
+ /** Request path without its query string, capped to what the ingestor accepts. */
34
+ function endpointPathOf(url) {
35
+ return (url.split('?')[0] || '/').slice(0, MAX_ENDPOINT_PATH);
36
+ }
37
+ /** First non-empty string among the candidates, or null. */
38
+ function firstNonEmpty(...values) {
39
+ for (const v of values) {
40
+ if (v === undefined || v === null)
41
+ continue;
42
+ const s = String(Array.isArray(v) ? v[0] : v).trim();
43
+ if (s)
44
+ return s;
45
+ }
46
+ return null;
47
+ }
48
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../../../src/middleware/common.ts"],"names":[],"mappings":";;;AAgBA,kCAEC;AAGD,8CAKC;AAMD,wCAEC;AAGD,sCAOC;AA1CD;;;;;;;GAOG;AACU,QAAA,mBAAmB,GAAG,WAAW,CAAC;AAE/C;;;GAGG;AACH,SAAgB,WAAW,CAAC,MAA0B;IACpD,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,SAAS,CAAC;AACpD,CAAC;AAED,wFAAwF;AACxF,SAAgB,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,2BAAmB,CAAC;IAC7D,CAAC;IACD,OAAO,OAAO,CAAC,UAAU,IAAI,2BAAmB,CAAC;AACnD,CAAC;AAED,iDAAiD;AACjD,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAE9B,kFAAkF;AAClF,SAAgB,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,SAAgB,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"}
@@ -0,0 +1,15 @@
1
+ import { MiddlewareOptions } from '../types';
2
+ /**
3
+ * Express middleware that automatically captures API usage events.
4
+ *
5
+ * Hooks into `res.on('finish')` — runs AFTER the response is sent to the client.
6
+ * Zero latency impact on the API call itself.
7
+ *
8
+ * ```typescript
9
+ * import { expressMiddleware } from '@aforoai/metering/middleware/express';
10
+ * app.use(expressMiddleware({ apiKey: process.env.AFORO_API_KEY, productType: 'API' }));
11
+ * ```
12
+ */
13
+ export declare function expressMiddleware(options: MiddlewareOptions): (req: any, res: any, next: any) => void;
14
+ export declare const middleware: typeof expressMiddleware;
15
+ //# sourceMappingURL=express.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../../src/middleware/express.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAK7C;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,IAUlB,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,GAAG,UAgEtE;AAeD,eAAO,MAAM,UAAU,0BAAoB,CAAC"}
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.middleware = void 0;
4
+ exports.expressMiddleware = expressMiddleware;
5
+ const client_1 = require("../client");
6
+ const common_1 = require("./common");
7
+ const DEFAULT_EXCLUDE_PATHS = ['/health', '/ready', '/metrics', '/favicon.ico'];
8
+ /**
9
+ * Express middleware that automatically captures API usage events.
10
+ *
11
+ * Hooks into `res.on('finish')` — runs AFTER the response is sent to the client.
12
+ * Zero latency impact on the API call itself.
13
+ *
14
+ * ```typescript
15
+ * import { expressMiddleware } from '@aforoai/metering/middleware/express';
16
+ * app.use(expressMiddleware({ apiKey: process.env.AFORO_API_KEY, productType: 'API' }));
17
+ * ```
18
+ */
19
+ function expressMiddleware(options) {
20
+ const client = new client_1.AforoClient({
21
+ apiKey: options.apiKey,
22
+ baseUrl: options.baseUrl,
23
+ ...options.clientOptions,
24
+ });
25
+ const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
26
+ const excludeStatusCodes = options.excludeStatusCodes ?? [];
27
+ return function aforoMeteringMiddleware(req, res, next) {
28
+ // Capture timing
29
+ const startTime = Date.now();
30
+ res.on('finish', () => {
31
+ try {
32
+ const path = req.originalUrl || req.url || '/';
33
+ const method = req.method || 'UNKNOWN';
34
+ const statusCode = res.statusCode || 0;
35
+ // Check exclusions
36
+ if ((0, common_1.isPreflight)(method))
37
+ return;
38
+ if (excludePaths.some((p) => path.startsWith(p)))
39
+ return;
40
+ if (excludeStatusCodes.includes(statusCode))
41
+ return;
42
+ // Resolve metric name (must be a metric in the tenant's catalog)
43
+ const metricName = (0, common_1.resolveMetricName)(options, req, res);
44
+ // Resolve quantity
45
+ let quantity;
46
+ if (typeof options.quantity === 'function') {
47
+ quantity = options.quantity(req, res);
48
+ }
49
+ else {
50
+ quantity = options.quantity ?? 1;
51
+ }
52
+ // Resolve customer ID
53
+ let customerId;
54
+ if (typeof options.customerId === 'function') {
55
+ customerId = options.customerId(req);
56
+ }
57
+ else if (options.customerId) {
58
+ customerId = options.customerId;
59
+ }
60
+ else {
61
+ customerId = extractCustomerId(req);
62
+ }
63
+ if (!customerId)
64
+ return; // Can't meter without a customer
65
+ if (!(quantity > 0))
66
+ return; // The ingestor rejects quantity <= 0
67
+ // Build metadata
68
+ let metadata;
69
+ if (options.metadata) {
70
+ metadata = options.metadata(req, res);
71
+ }
72
+ client.track({
73
+ customerId,
74
+ metricName,
75
+ quantity,
76
+ metadata,
77
+ productType: options.productType,
78
+ endpointPath: (0, common_1.endpointPathOf)(path),
79
+ httpMethod: method,
80
+ statusCode,
81
+ responseTimeMs: Date.now() - startTime,
82
+ }).catch(() => { }); // Fire-and-forget
83
+ }
84
+ catch {
85
+ // Never let metering errors affect the API
86
+ }
87
+ });
88
+ next();
89
+ };
90
+ }
91
+ /**
92
+ * Default customer resolution: authenticated user id, then X-Customer-Id.
93
+ *
94
+ * The caller's X-Api-Key header is deliberately NOT used: that is the end
95
+ * user's secret, and using it as customerId wrote credentials into billing data
96
+ * while never matching an Aforo customer. Configure `customerId` to map your
97
+ * callers to Aforo customer ids.
98
+ */
99
+ function extractCustomerId(req) {
100
+ return (0, common_1.firstNonEmpty)(req.user?.id, req.user?.sub, req.headers?.['x-customer-id']);
101
+ }
102
+ // Also export as `middleware` for convenience
103
+ exports.middleware = expressMiddleware;
104
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../../src/middleware/express.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { MiddlewareOptions } from '../types';
2
+ /**
3
+ * Fastify plugin that automatically captures API usage events.
4
+ *
5
+ * Uses the `onResponse` hook — runs AFTER the response is sent.
6
+ *
7
+ * ```typescript
8
+ * import { fastifyPlugin } from '@aforoai/metering/middleware/fastify';
9
+ * fastify.register(fastifyPlugin, { apiKey: process.env.AFORO_API_KEY, productType: 'API' });
10
+ * ```
11
+ */
12
+ export declare function fastifyPlugin(fastify: any, options: MiddlewareOptions): Promise<void>;
13
+ //# sourceMappingURL=fastify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../../../src/middleware/fastify.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAK7C;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,iBAAiB,iBAwE3E"}
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fastifyPlugin = fastifyPlugin;
4
+ const client_1 = require("../client");
5
+ const common_1 = require("./common");
6
+ const DEFAULT_EXCLUDE_PATHS = ['/health', '/ready', '/metrics', '/favicon.ico'];
7
+ /**
8
+ * Fastify plugin that automatically captures API usage events.
9
+ *
10
+ * Uses the `onResponse` hook — runs AFTER the response is sent.
11
+ *
12
+ * ```typescript
13
+ * import { fastifyPlugin } from '@aforoai/metering/middleware/fastify';
14
+ * fastify.register(fastifyPlugin, { apiKey: process.env.AFORO_API_KEY, productType: 'API' });
15
+ * ```
16
+ */
17
+ async function fastifyPlugin(fastify, options) {
18
+ const client = new client_1.AforoClient({
19
+ apiKey: options.apiKey,
20
+ baseUrl: options.baseUrl,
21
+ ...options.clientOptions,
22
+ });
23
+ const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
24
+ const excludeStatusCodes = options.excludeStatusCodes ?? [];
25
+ fastify.addHook('onResponse', (request, reply, done) => {
26
+ try {
27
+ const path = request.url || '/';
28
+ const method = request.method || 'UNKNOWN';
29
+ const statusCode = reply.statusCode || 0;
30
+ if ((0, common_1.isPreflight)(method))
31
+ return done();
32
+ if (excludePaths.some((p) => path.startsWith(p)))
33
+ return done();
34
+ if (excludeStatusCodes.includes(statusCode))
35
+ return done();
36
+ const metricName = (0, common_1.resolveMetricName)(options, request, reply);
37
+ let quantity;
38
+ if (typeof options.quantity === 'function') {
39
+ quantity = options.quantity(request, reply);
40
+ }
41
+ else {
42
+ quantity = options.quantity ?? 1;
43
+ }
44
+ let customerId;
45
+ if (typeof options.customerId === 'function') {
46
+ customerId = options.customerId(request);
47
+ }
48
+ else if (options.customerId) {
49
+ customerId = options.customerId;
50
+ }
51
+ else {
52
+ // Never the caller's X-Api-Key: that is a secret, not a customer id.
53
+ customerId = (0, common_1.firstNonEmpty)(request.user?.id, request.headers?.['x-customer-id']);
54
+ }
55
+ if (!customerId)
56
+ return done();
57
+ if (!(quantity > 0))
58
+ return done(); // The ingestor rejects quantity <= 0
59
+ let metadata;
60
+ if (options.metadata) {
61
+ metadata = options.metadata(request, reply);
62
+ }
63
+ const elapsed = typeof reply.elapsedTime === 'number'
64
+ ? reply.elapsedTime
65
+ : typeof reply.getResponseTime === 'function' ? reply.getResponseTime() : undefined;
66
+ client.track({
67
+ customerId,
68
+ metricName,
69
+ quantity,
70
+ metadata,
71
+ productType: options.productType,
72
+ endpointPath: (0, common_1.endpointPathOf)(path),
73
+ httpMethod: method,
74
+ statusCode,
75
+ ...(typeof elapsed === 'number' ? { responseTimeMs: Math.round(elapsed) } : {}),
76
+ }).catch(() => { });
77
+ }
78
+ catch {
79
+ // Never let metering affect the API
80
+ }
81
+ done();
82
+ });
83
+ fastify.addHook('onClose', async () => {
84
+ await client.shutdown();
85
+ });
86
+ }
87
+ // Mark as fastify plugin
88
+ fastifyPlugin[Symbol.for('skip-override')] = true;
89
+ fastifyPlugin[Symbol.for('fastify.display-name')] = 'aforo-metering';
90
+ //# sourceMappingURL=fastify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../../src/middleware/fastify.ts"],"names":[],"mappings":"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"}