@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.
- package/README.md +88 -0
- package/dist/cjs/buffer.d.ts +27 -0
- package/dist/cjs/buffer.d.ts.map +1 -0
- package/dist/cjs/buffer.js +75 -0
- package/dist/cjs/buffer.js.map +1 -0
- package/dist/cjs/client.d.ts +86 -0
- package/dist/cjs/client.d.ts.map +1 -0
- package/dist/cjs/client.js +301 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/idempotency.d.ts +21 -0
- package/dist/cjs/idempotency.d.ts.map +1 -0
- package/dist/cjs/idempotency.js +31 -0
- package/dist/cjs/idempotency.js.map +1 -0
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +23 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/limits.d.ts +80 -0
- package/dist/cjs/limits.d.ts.map +1 -0
- package/dist/cjs/limits.js +143 -0
- package/dist/cjs/limits.js.map +1 -0
- package/dist/cjs/middleware/common.d.ts +22 -0
- package/dist/cjs/middleware/common.d.ts.map +1 -0
- package/dist/cjs/middleware/common.js +48 -0
- package/dist/cjs/middleware/common.js.map +1 -0
- package/dist/cjs/middleware/express.d.ts +15 -0
- package/dist/cjs/middleware/express.d.ts.map +1 -0
- package/dist/cjs/middleware/express.js +104 -0
- package/dist/cjs/middleware/express.js.map +1 -0
- package/dist/cjs/middleware/fastify.d.ts +13 -0
- package/dist/cjs/middleware/fastify.d.ts.map +1 -0
- package/dist/cjs/middleware/fastify.js +90 -0
- package/dist/cjs/middleware/fastify.js.map +1 -0
- package/dist/cjs/middleware/koa.d.ts +13 -0
- package/dist/cjs/middleware/koa.d.ts.map +1 -0
- package/dist/cjs/middleware/koa.js +82 -0
- package/dist/cjs/middleware/koa.js.map +1 -0
- package/dist/cjs/path-normalizer.d.ts +15 -0
- package/dist/cjs/path-normalizer.d.ts.map +1 -0
- package/dist/cjs/path-normalizer.js +49 -0
- package/dist/cjs/path-normalizer.js.map +1 -0
- package/dist/cjs/transport.d.ts +39 -0
- package/dist/cjs/transport.d.ts.map +1 -0
- package/dist/cjs/transport.js +116 -0
- package/dist/cjs/transport.js.map +1 -0
- package/dist/cjs/types.d.ts +128 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/buffer.js +71 -0
- package/dist/esm/buffer.js.map +1 -0
- package/dist/esm/client.js +297 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/idempotency.js +27 -0
- package/dist/esm/idempotency.js.map +1 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/limits.js +138 -0
- package/dist/esm/limits.js.map +1 -0
- package/dist/esm/middleware/common.js +41 -0
- package/dist/esm/middleware/common.js.map +1 -0
- package/dist/esm/middleware/express.js +100 -0
- package/dist/esm/middleware/express.js.map +1 -0
- package/dist/esm/middleware/fastify.js +87 -0
- package/dist/esm/middleware/fastify.js.map +1 -0
- package/dist/esm/middleware/koa.js +79 -0
- package/dist/esm/middleware/koa.js.map +1 -0
- package/dist/esm/path-normalizer.js +46 -0
- package/dist/esm/path-normalizer.js.map +1 -0
- package/dist/esm/transport.js +112 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/buffer.d.ts +27 -0
- package/dist/types/buffer.d.ts.map +1 -0
- package/dist/types/client.d.ts +86 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/idempotency.d.ts +21 -0
- package/dist/types/idempotency.d.ts.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/limits.d.ts +80 -0
- package/dist/types/limits.d.ts.map +1 -0
- package/dist/types/middleware/common.d.ts +22 -0
- package/dist/types/middleware/common.d.ts.map +1 -0
- package/dist/types/middleware/express.d.ts +15 -0
- package/dist/types/middleware/express.d.ts.map +1 -0
- package/dist/types/middleware/fastify.d.ts +13 -0
- package/dist/types/middleware/fastify.d.ts.map +1 -0
- package/dist/types/middleware/koa.d.ts +13 -0
- package/dist/types/middleware/koa.d.ts.map +1 -0
- package/dist/types/path-normalizer.d.ts +15 -0
- package/dist/types/path-normalizer.d.ts.map +1 -0
- package/dist/types/transport.d.ts +39 -0
- package/dist/types/transport.d.ts.map +1 -0
- package/dist/types/types.d.ts +128 -0
- package/dist/types/types.d.ts.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { RingBuffer } from './buffer';
|
|
2
|
+
import { Transport } from './transport';
|
|
3
|
+
import { generateRandomKey } from './idempotency';
|
|
4
|
+
import { describeLimitViolation } from './limits';
|
|
5
|
+
const DEFAULT_BASE_URL = 'https://api.aforo.ai';
|
|
6
|
+
const DEFAULT_PRODUCT_TYPE = 'API';
|
|
7
|
+
const DEFAULT_SESSION_PRODUCT_TYPE = 'AI_AGENT';
|
|
8
|
+
/** Ingestor hard limit on events per batch request (IngestBatchRequest @Size(max = 1000)). */
|
|
9
|
+
const MAX_BATCH_SIZE = 1000;
|
|
10
|
+
const HEARTBEAT_INTERVAL_MS = 30000;
|
|
11
|
+
const HEARTBEAT_METRIC = 'system.session.heartbeat';
|
|
12
|
+
/** Heartbeats carry no billable customer; the ingestor intercepts them before billing. */
|
|
13
|
+
const HEARTBEAT_CUSTOMER_ID = 'system';
|
|
14
|
+
const DEFAULT_FLUSH_COUNT = 50;
|
|
15
|
+
const DEFAULT_FLUSH_INTERVAL = 5000;
|
|
16
|
+
const DEFAULT_MAX_QUEUE_SIZE = 10000;
|
|
17
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
18
|
+
const DEFAULT_RETRY_BASE_MS = 1000;
|
|
19
|
+
const DEFAULT_TIMEOUT = 10000;
|
|
20
|
+
const DEFAULT_SHUTDOWN_TIMEOUT = 5000;
|
|
21
|
+
/**
|
|
22
|
+
* Aforo metering client.
|
|
23
|
+
*
|
|
24
|
+
* Enqueues usage events into a ring buffer, flushes them in batches
|
|
25
|
+
* to the Aforo ingestor service via HTTP. Non-blocking — `track()`
|
|
26
|
+
* returns immediately, flushing happens in the background.
|
|
27
|
+
*
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const client = new AforoClient({ apiKey: 'your-key', productType: 'API' });
|
|
30
|
+
* await client.track({ customerId: 'cust_1', metricName: 'api_calls', quantity: 1 });
|
|
31
|
+
* // On shutdown:
|
|
32
|
+
* await client.shutdown();
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class AforoClient {
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.flushTimer = null;
|
|
38
|
+
this.flushing = false;
|
|
39
|
+
this.closed = false;
|
|
40
|
+
this.pendingFlush = null;
|
|
41
|
+
// Session heartbeat state
|
|
42
|
+
this.heartbeatTimer = null;
|
|
43
|
+
this.activeSessionId = null;
|
|
44
|
+
this.sessionStartedAt = null;
|
|
45
|
+
this.sessionProductType = DEFAULT_SESSION_PRODUCT_TYPE;
|
|
46
|
+
if (!options.apiKey)
|
|
47
|
+
throw new Error('apiKey is required');
|
|
48
|
+
this.flushCount = Math.max(1, Math.min(options.flushCount ?? DEFAULT_FLUSH_COUNT, MAX_BATCH_SIZE));
|
|
49
|
+
this.productType = normalizeProductType(options.productType) ?? DEFAULT_PRODUCT_TYPE;
|
|
50
|
+
this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
|
|
51
|
+
this.shutdownTimeoutMs = options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT;
|
|
52
|
+
this.buffer = new RingBuffer(options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE);
|
|
53
|
+
this.transport = new Transport({
|
|
54
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
55
|
+
apiKey: options.apiKey,
|
|
56
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT,
|
|
57
|
+
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
58
|
+
retryBaseMs: options.retryBaseMs ?? DEFAULT_RETRY_BASE_MS,
|
|
59
|
+
});
|
|
60
|
+
// Start periodic flush timer
|
|
61
|
+
this.flushTimer = setInterval(() => {
|
|
62
|
+
this.flush().catch(() => { });
|
|
63
|
+
}, this.flushInterval);
|
|
64
|
+
// Unref the timer so it doesn't prevent Node from exiting
|
|
65
|
+
if (this.flushTimer && typeof this.flushTimer === 'object' && 'unref' in this.flushTimer) {
|
|
66
|
+
this.flushTimer.unref();
|
|
67
|
+
}
|
|
68
|
+
// Register graceful shutdown handlers
|
|
69
|
+
const shutdownHandler = () => {
|
|
70
|
+
this.shutdown().catch(() => { });
|
|
71
|
+
};
|
|
72
|
+
process.once('SIGTERM', shutdownHandler);
|
|
73
|
+
process.once('SIGINT', shutdownHandler);
|
|
74
|
+
}
|
|
75
|
+
// ─── Session lifecycle with heartbeat ─────────────────────────────
|
|
76
|
+
/**
|
|
77
|
+
* Start a session and emit `system.session.heartbeat` events: one now, then
|
|
78
|
+
* every 30s until `endSession()` / `shutdown()`.
|
|
79
|
+
*
|
|
80
|
+
* Each heartbeat is POSTed in its own request (`{"events":[heartbeat]}`),
|
|
81
|
+
* never mixed into a usage batch, so it always takes the ingestor's
|
|
82
|
+
* synchronous path, where it is intercepted before billing. Heartbeats are
|
|
83
|
+
* best-effort: sent once, failures ignored, never affecting usage delivery.
|
|
84
|
+
* The timer is unref'd so it never keeps the process alive.
|
|
85
|
+
*
|
|
86
|
+
* @param sessionId - Unique session identifier
|
|
87
|
+
* @param productType - Product type for the session (default AI_AGENT)
|
|
88
|
+
*/
|
|
89
|
+
startSession(sessionId, productType = DEFAULT_SESSION_PRODUCT_TYPE) {
|
|
90
|
+
if (this.closed || !sessionId || !String(sessionId).trim())
|
|
91
|
+
return;
|
|
92
|
+
this.stopHeartbeatTimer();
|
|
93
|
+
this.activeSessionId = String(sessionId);
|
|
94
|
+
this.sessionStartedAt = Date.now();
|
|
95
|
+
this.sessionProductType = normalizeProductType(productType) ?? DEFAULT_SESSION_PRODUCT_TYPE;
|
|
96
|
+
this.emitSessionHeartbeat('HEARTBEAT');
|
|
97
|
+
this.heartbeatTimer = setInterval(() => this.emitSessionHeartbeat('HEARTBEAT'), HEARTBEAT_INTERVAL_MS);
|
|
98
|
+
if (this.heartbeatTimer && typeof this.heartbeatTimer === 'object' && 'unref' in this.heartbeatTimer) {
|
|
99
|
+
this.heartbeatTimer.unref();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* End the current session: stop heartbeats, send a final SESSION_END
|
|
104
|
+
* heartbeat (in its own request, best-effort) and flush buffered usage.
|
|
105
|
+
*/
|
|
106
|
+
async endSession() {
|
|
107
|
+
this.stopHeartbeatTimer();
|
|
108
|
+
const end = this.activeSessionId ? this.emitSessionHeartbeat('SESSION_END') : Promise.resolve();
|
|
109
|
+
this.activeSessionId = null;
|
|
110
|
+
this.sessionStartedAt = null;
|
|
111
|
+
await Promise.all([end, this.flush()]);
|
|
112
|
+
}
|
|
113
|
+
stopHeartbeatTimer() {
|
|
114
|
+
if (this.heartbeatTimer) {
|
|
115
|
+
clearInterval(this.heartbeatTimer);
|
|
116
|
+
this.heartbeatTimer = null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
emitSessionHeartbeat(boundary) {
|
|
120
|
+
const sessionId = this.activeSessionId;
|
|
121
|
+
if (!sessionId || (this.closed && boundary === 'HEARTBEAT'))
|
|
122
|
+
return Promise.resolve();
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const occurredAt = new Date(now).toISOString();
|
|
125
|
+
const heartbeat = {
|
|
126
|
+
customerId: HEARTBEAT_CUSTOMER_ID,
|
|
127
|
+
metricName: HEARTBEAT_METRIC,
|
|
128
|
+
// quantity must be > 0 to pass the ingestor's bean validation; the
|
|
129
|
+
// heartbeat is intercepted before billing and never counted as usage.
|
|
130
|
+
quantity: 1,
|
|
131
|
+
idempotencyKey: `hb:${boundary === 'SESSION_END' ? 'end:' : ''}${sessionId}:${now}:${randomSuffix()}`.slice(0, 255),
|
|
132
|
+
occurredAt,
|
|
133
|
+
productType: this.sessionProductType,
|
|
134
|
+
sessionId,
|
|
135
|
+
sessionBoundary: boundary,
|
|
136
|
+
metadata: {
|
|
137
|
+
sessionId,
|
|
138
|
+
sessionBoundary: boundary,
|
|
139
|
+
productType: this.sessionProductType,
|
|
140
|
+
heartbeatType: boundary === 'SESSION_END' ? 'SESSION_END' : 'PERIODIC',
|
|
141
|
+
uptimeMs: now - (this.sessionStartedAt ?? now),
|
|
142
|
+
sdkLanguage: 'node',
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
return this.transport.sendSingleBestEffort(heartbeat).then(() => undefined, () => undefined);
|
|
146
|
+
}
|
|
147
|
+
// ─── Event tracking ──────────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* Enqueue a usage event for batched delivery.
|
|
150
|
+
* Returns immediately — does not await HTTP.
|
|
151
|
+
* Triggers a flush if the buffer reaches flushCount.
|
|
152
|
+
*
|
|
153
|
+
* Throws if the event breaks any constraint the ingestor enforces — a blank
|
|
154
|
+
* or over-long `customerId` / `metricName` / `idempotencyKey`, a `quantity`
|
|
155
|
+
* that is not positive or carries more than 14 integer digits / 6 decimal
|
|
156
|
+
* places, an `occurredAt` that is not an ISO-8601 instant, or any other
|
|
157
|
+
* capped field that is too long. Limits the server makes configurable (event
|
|
158
|
+
* age, clock skew, metadata size) are left to the server, so the SDK can
|
|
159
|
+
* never refuse usage a given deployment would accept. Such an event is rejected by the ingestor and never billed,
|
|
160
|
+
* and because flushing happens in the background nobody would see that
|
|
161
|
+
* rejection — so it is reported here instead, and never enters the buffer.
|
|
162
|
+
* See `limits.ts` for the limits and where each one comes from.
|
|
163
|
+
*/
|
|
164
|
+
async track(event) {
|
|
165
|
+
if (this.closed) {
|
|
166
|
+
throw new Error('AforoClient is shut down — cannot track new events');
|
|
167
|
+
}
|
|
168
|
+
if (event.customerId === undefined || event.customerId === null || !String(event.customerId).trim()) {
|
|
169
|
+
throw new Error('customerId is required');
|
|
170
|
+
}
|
|
171
|
+
if (event.metricName === undefined || event.metricName === null || !String(event.metricName).trim()) {
|
|
172
|
+
throw new Error('metricName is required');
|
|
173
|
+
}
|
|
174
|
+
const occurredAt = resolveOccurredAt(event.occurredAt);
|
|
175
|
+
const quantity = event.quantity ?? 1;
|
|
176
|
+
if (typeof quantity !== 'number' || !Number.isFinite(quantity) || quantity <= 0) {
|
|
177
|
+
throw new Error('quantity must be a positive number (> 0)');
|
|
178
|
+
}
|
|
179
|
+
const productType = normalizeProductType(event.productType) ?? this.productType;
|
|
180
|
+
// Minted once, here, when the event is enqueued — never at flush/retry
|
|
181
|
+
// time, so a retried batch carries the same keys and the ingestor
|
|
182
|
+
// deduplicates it. A caller-supplied key is passed through verbatim;
|
|
183
|
+
// otherwise each event gets its own random UUID. A deterministic hash of
|
|
184
|
+
// the event fields would make two genuinely distinct events in the same
|
|
185
|
+
// millisecond collide, and the ingestor would silently drop the second.
|
|
186
|
+
// Minted before the limit check so an over-long caller-supplied key is
|
|
187
|
+
// reported here rather than silently dropped by the server later.
|
|
188
|
+
const idempotencyKey = event.idempotencyKey ?? generateRandomKey();
|
|
189
|
+
const violation = describeLimitViolation({
|
|
190
|
+
customerId: event.customerId,
|
|
191
|
+
metricName: event.metricName,
|
|
192
|
+
quantity,
|
|
193
|
+
idempotencyKey,
|
|
194
|
+
occurredAt,
|
|
195
|
+
productType,
|
|
196
|
+
endpointPath: event.endpointPath,
|
|
197
|
+
httpMethod: event.httpMethod,
|
|
198
|
+
});
|
|
199
|
+
if (violation)
|
|
200
|
+
throw new Error(violation);
|
|
201
|
+
const resolved = {
|
|
202
|
+
customerId: event.customerId,
|
|
203
|
+
metricName: event.metricName,
|
|
204
|
+
quantity,
|
|
205
|
+
idempotencyKey,
|
|
206
|
+
occurredAt,
|
|
207
|
+
productType,
|
|
208
|
+
...(event.metadata ? { metadata: event.metadata } : {}),
|
|
209
|
+
...(event.endpointPath !== undefined ? { endpointPath: event.endpointPath } : {}),
|
|
210
|
+
...(event.httpMethod !== undefined ? { httpMethod: event.httpMethod } : {}),
|
|
211
|
+
...(event.statusCode !== undefined ? { statusCode: event.statusCode } : {}),
|
|
212
|
+
...(event.responseTimeMs !== undefined ? { responseTimeMs: event.responseTimeMs } : {}),
|
|
213
|
+
};
|
|
214
|
+
this.buffer.push(resolved);
|
|
215
|
+
// Trigger flush if buffer threshold reached
|
|
216
|
+
if (this.buffer.size >= this.flushCount) {
|
|
217
|
+
this.flush().catch(() => { });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Force-flush all buffered events to the ingestor.
|
|
222
|
+
* Safe to call concurrently — only one flush runs at a time.
|
|
223
|
+
*/
|
|
224
|
+
async flush() {
|
|
225
|
+
if (this.flushing && this.pendingFlush) {
|
|
226
|
+
return this.pendingFlush;
|
|
227
|
+
}
|
|
228
|
+
this.flushing = true;
|
|
229
|
+
this.pendingFlush = this.doFlush();
|
|
230
|
+
try {
|
|
231
|
+
return await this.pendingFlush;
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
this.flushing = false;
|
|
235
|
+
this.pendingFlush = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async doFlush() {
|
|
239
|
+
let totalSent = 0;
|
|
240
|
+
let totalFailed = 0;
|
|
241
|
+
while (!this.buffer.isEmpty) {
|
|
242
|
+
const batch = this.buffer.drainUpTo(this.flushCount);
|
|
243
|
+
if (batch.length === 0)
|
|
244
|
+
break;
|
|
245
|
+
const result = await this.transport.send(batch);
|
|
246
|
+
totalSent += result.sent;
|
|
247
|
+
totalFailed += result.failed;
|
|
248
|
+
}
|
|
249
|
+
return { sent: totalSent, failed: totalFailed };
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Flush remaining events and stop the client.
|
|
253
|
+
* After shutdown, track() will throw.
|
|
254
|
+
*/
|
|
255
|
+
async shutdown() {
|
|
256
|
+
if (this.closed)
|
|
257
|
+
return;
|
|
258
|
+
this.closed = true;
|
|
259
|
+
// Stop session heartbeats
|
|
260
|
+
this.stopHeartbeatTimer();
|
|
261
|
+
// Clear periodic flush timer
|
|
262
|
+
if (this.flushTimer) {
|
|
263
|
+
clearInterval(this.flushTimer);
|
|
264
|
+
this.flushTimer = null;
|
|
265
|
+
}
|
|
266
|
+
// Flush with timeout
|
|
267
|
+
await Promise.race([
|
|
268
|
+
this.flush(),
|
|
269
|
+
new Promise((resolve) => setTimeout(resolve, this.shutdownTimeoutMs)),
|
|
270
|
+
]);
|
|
271
|
+
}
|
|
272
|
+
/** Number of events currently buffered. */
|
|
273
|
+
get bufferedCount() {
|
|
274
|
+
return this.buffer.size;
|
|
275
|
+
}
|
|
276
|
+
/** Whether the client has been shut down. */
|
|
277
|
+
get isShutdown() {
|
|
278
|
+
return this.closed;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function normalizeProductType(value) {
|
|
282
|
+
if (value === undefined || value === null)
|
|
283
|
+
return undefined;
|
|
284
|
+
const trimmed = String(value).trim();
|
|
285
|
+
return trimmed ? trimmed.toUpperCase() : undefined;
|
|
286
|
+
}
|
|
287
|
+
function randomSuffix() {
|
|
288
|
+
return Math.random().toString(36).slice(2, 10);
|
|
289
|
+
}
|
|
290
|
+
function resolveOccurredAt(value) {
|
|
291
|
+
if (!value)
|
|
292
|
+
return new Date().toISOString();
|
|
293
|
+
if (typeof value === 'number')
|
|
294
|
+
return new Date(value).toISOString();
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a deterministic idempotency key from event fields.
|
|
4
|
+
* Uses SHA-256, truncated to 32 hex chars for compact storage.
|
|
5
|
+
*
|
|
6
|
+
* Formula: SHA256(customerId + metricName + quantity + occurredAt)
|
|
7
|
+
*
|
|
8
|
+
* NOT the client default any more. `occurredAt` only carries millisecond
|
|
9
|
+
* precision, so two genuinely distinct events for the same customer + metric
|
|
10
|
+
* + quantity inside one millisecond hash to the same key and the ingestor
|
|
11
|
+
* drops the second as a DUPLICATE — silent under-billing. Kept exported for
|
|
12
|
+
* callers who deliberately want content-addressed dedup (e.g. replaying a
|
|
13
|
+
* fixed batch) and pass the result to `track({ idempotencyKey })` themselves.
|
|
14
|
+
*/
|
|
15
|
+
export function generateIdempotencyKey(customerId, metricName, quantity, occurredAt) {
|
|
16
|
+
const input = `${customerId}:${metricName}:${quantity}:${occurredAt}`;
|
|
17
|
+
return createHash('sha256').update(input).digest('hex').substring(0, 32);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate a random UUID v4. This is the default key for an event whose caller
|
|
21
|
+
* supplied none: every event gets its own key, so no two distinct events can
|
|
22
|
+
* collide. Dedup stays opt-in via an explicit `idempotencyKey`.
|
|
23
|
+
*/
|
|
24
|
+
export function generateRandomKey() {
|
|
25
|
+
return randomUUID();
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=idempotency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../../src/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,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,UAAU,CAAC,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,MAAM,UAAU,iBAAiB;IAC/B,OAAO,UAAU,EAAE,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Public API for @aforoai/metering
|
|
2
|
+
export { AforoClient } from './client';
|
|
3
|
+
export { expressMiddleware, middleware } from './middleware/express';
|
|
4
|
+
export { fastifyPlugin } from './middleware/fastify';
|
|
5
|
+
export { koaMiddleware } from './middleware/koa';
|
|
6
|
+
export { normalizePath } from './path-normalizer';
|
|
7
|
+
export { DEFAULT_METRIC_NAME } from './middleware/common';
|
|
8
|
+
export { MAX_LENGTHS, MAX_QUANTITY_DECIMAL_PLACES, MAX_QUANTITY_INTEGER_DIGITS, describeLimitViolation, } from './limits';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAEnC,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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
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 const MAX_LENGTHS = {
|
|
30
|
+
customerId: 64,
|
|
31
|
+
metricName: 255,
|
|
32
|
+
idempotencyKey: 255,
|
|
33
|
+
productType: 20,
|
|
34
|
+
traceId: 128,
|
|
35
|
+
spanId: 32,
|
|
36
|
+
sessionId: 64,
|
|
37
|
+
agentId: 36,
|
|
38
|
+
toolName: 64,
|
|
39
|
+
endpointPath: 512,
|
|
40
|
+
httpMethod: 16,
|
|
41
|
+
};
|
|
42
|
+
/** `@Digits(integer = 14, fraction = 6)` — `usage_events.quantity` is NUMERIC(20,6). */
|
|
43
|
+
export const MAX_QUANTITY_INTEGER_DIGITS = 14;
|
|
44
|
+
export const MAX_QUANTITY_DECIMAL_PLACES = 6;
|
|
45
|
+
/**
|
|
46
|
+
* ISO-8601 date-time, the shape Jackson can read into an `Instant`. The zone
|
|
47
|
+
* designator is optional here: the ingestor's example values all carry `Z`, but
|
|
48
|
+
* an offset-less value is not provably rejected, and falsely rejecting one
|
|
49
|
+
* would drop a billable event.
|
|
50
|
+
*/
|
|
51
|
+
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})?$/;
|
|
52
|
+
/**
|
|
53
|
+
* Length as the ingestor counts it: `String.length()` in Java is UTF-16 code
|
|
54
|
+
* units, which is exactly what `String.prototype.length` returns.
|
|
55
|
+
*/
|
|
56
|
+
function lengthOf(value) {
|
|
57
|
+
return value.length;
|
|
58
|
+
}
|
|
59
|
+
function lengthError(field, value) {
|
|
60
|
+
if (value === undefined || value === null)
|
|
61
|
+
return null;
|
|
62
|
+
const text = String(value);
|
|
63
|
+
const max = MAX_LENGTHS[field];
|
|
64
|
+
const len = lengthOf(text);
|
|
65
|
+
if (len <= max)
|
|
66
|
+
return null;
|
|
67
|
+
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, `
|
|
69
|
+
+ 'because a truncated id bills the wrong thing.';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Integer and decimal digit counts as the ingestor's `@Digits` sees them, i.e.
|
|
73
|
+
* as `BigDecimal` reads the number the SDK puts on the wire:
|
|
74
|
+
* `integerDigits = precision - scale`, `decimalPlaces = max(scale, 0)`.
|
|
75
|
+
*
|
|
76
|
+
* Works from the serialized text so exponent notation (`1e+21`, which
|
|
77
|
+
* `JSON.stringify` emits for large numbers) is counted the same way the server
|
|
78
|
+
* will count it.
|
|
79
|
+
*/
|
|
80
|
+
export function quantityDigits(serialized) {
|
|
81
|
+
const match = /^[+-]?(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(serialized.trim());
|
|
82
|
+
if (!match)
|
|
83
|
+
return { integerDigits: 0, decimalPlaces: 0 };
|
|
84
|
+
const intPart = match[1] ?? '';
|
|
85
|
+
const fracPart = match[2] ?? '';
|
|
86
|
+
const exponent = match[3] ? parseInt(match[3], 10) : 0;
|
|
87
|
+
const digits = (intPart + fracPart).replace(/^0+/, '');
|
|
88
|
+
const precision = digits.length === 0 ? 1 : digits.length;
|
|
89
|
+
const scale = fracPart.length - exponent;
|
|
90
|
+
return { integerDigits: precision - scale, decimalPlaces: Math.max(scale, 0) };
|
|
91
|
+
}
|
|
92
|
+
function quantityError(quantity) {
|
|
93
|
+
// JSON.stringify is what the transport uses, so this is the exact text the
|
|
94
|
+
// ingestor will parse into the BigDecimal that @Digits inspects.
|
|
95
|
+
const serialized = JSON.stringify(quantity);
|
|
96
|
+
const { integerDigits, decimalPlaces } = quantityDigits(String(serialized));
|
|
97
|
+
if (integerDigits > MAX_QUANTITY_INTEGER_DIGITS) {
|
|
98
|
+
return `quantity ${serialized} has ${integerDigits} integer digits, exceeding the ingestor's `
|
|
99
|
+
+ `limit of ${MAX_QUANTITY_INTEGER_DIGITS} (usage_events.quantity is NUMERIC(20,6)).`;
|
|
100
|
+
}
|
|
101
|
+
if (decimalPlaces > MAX_QUANTITY_DECIMAL_PLACES) {
|
|
102
|
+
return `quantity ${serialized} has ${decimalPlaces} decimal places, exceeding the ingestor's `
|
|
103
|
+
+ `limit of ${MAX_QUANTITY_DECIMAL_PLACES}. Round it yourself before tracking — the SDK will `
|
|
104
|
+
+ 'not round it, because that would change the quantity you are billed for.';
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function occurredAtError(occurredAt) {
|
|
109
|
+
if (!ISO_DATE_TIME.test(occurredAt)) {
|
|
110
|
+
return `occurredAt "${occurredAt}" is not an ISO-8601 timestamp `
|
|
111
|
+
+ '(expected e.g. "2026-03-01T14:30:00Z").';
|
|
112
|
+
}
|
|
113
|
+
if (Number.isNaN(Date.parse(occurredAt))) {
|
|
114
|
+
return `occurredAt "${occurredAt}" is not a valid date.`;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 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.
|
|
122
|
+
*/
|
|
123
|
+
export function describeLimitViolation(event) {
|
|
124
|
+
if (event.idempotencyKey !== undefined && event.idempotencyKey !== null
|
|
125
|
+
&& !String(event.idempotencyKey).trim()) {
|
|
126
|
+
return 'idempotencyKey must not be blank when provided.';
|
|
127
|
+
}
|
|
128
|
+
for (const field of Object.keys(MAX_LENGTHS)) {
|
|
129
|
+
const problem = lengthError(field, event[field]);
|
|
130
|
+
if (problem)
|
|
131
|
+
return problem;
|
|
132
|
+
}
|
|
133
|
+
const quantityProblem = quantityError(event.quantity);
|
|
134
|
+
if (quantityProblem)
|
|
135
|
+
return quantityProblem;
|
|
136
|
+
return occurredAtError(event.occurredAt);
|
|
137
|
+
}
|
|
138
|
+
//# 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,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metric recorded for each request when `metricName` is not configured.
|
|
3
|
+
*
|
|
4
|
+
* It must exist in the tenant's Aforo metric catalog: the ingestor rejects an
|
|
5
|
+
* unknown metric, and because it validates a batch as a whole, one such event
|
|
6
|
+
* fails the entire batch with 400. The previous default, `"METHOD /path"`, is a
|
|
7
|
+
* name no catalog contains, so every event failed out of the box.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_METRIC_NAME = 'api_calls';
|
|
10
|
+
/**
|
|
11
|
+
* CORS preflights are a browser protocol detail, not a billable call, and carry
|
|
12
|
+
* no credentials -- so they never have a customer. They are never metered.
|
|
13
|
+
*/
|
|
14
|
+
export function isPreflight(method) {
|
|
15
|
+
return (method || '').toUpperCase() === 'OPTIONS';
|
|
16
|
+
}
|
|
17
|
+
/** Resolve the metric: resolver function, then fixed name, then DEFAULT_METRIC_NAME. */
|
|
18
|
+
export function resolveMetricName(options, req, res) {
|
|
19
|
+
if (typeof options.metricName === 'function') {
|
|
20
|
+
return options.metricName(req, res) || DEFAULT_METRIC_NAME;
|
|
21
|
+
}
|
|
22
|
+
return options.metricName || DEFAULT_METRIC_NAME;
|
|
23
|
+
}
|
|
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
|
+
export function endpointPathOf(url) {
|
|
28
|
+
return (url.split('?')[0] || '/').slice(0, MAX_ENDPOINT_PATH);
|
|
29
|
+
}
|
|
30
|
+
/** First non-empty string among the candidates, or null. */
|
|
31
|
+
export function firstNonEmpty(...values) {
|
|
32
|
+
for (const v of values) {
|
|
33
|
+
if (v === undefined || v === null)
|
|
34
|
+
continue;
|
|
35
|
+
const s = String(Array.isArray(v) ? v[0] : v).trim();
|
|
36
|
+
if (s)
|
|
37
|
+
return s;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=common.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { AforoClient } from '../client';
|
|
2
|
+
import { isPreflight, resolveMetricName, firstNonEmpty, endpointPathOf } from './common';
|
|
3
|
+
const DEFAULT_EXCLUDE_PATHS = ['/health', '/ready', '/metrics', '/favicon.ico'];
|
|
4
|
+
/**
|
|
5
|
+
* Express middleware that automatically captures API usage events.
|
|
6
|
+
*
|
|
7
|
+
* Hooks into `res.on('finish')` — runs AFTER the response is sent to the client.
|
|
8
|
+
* Zero latency impact on the API call itself.
|
|
9
|
+
*
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { expressMiddleware } from '@aforoai/metering/middleware/express';
|
|
12
|
+
* app.use(expressMiddleware({ apiKey: process.env.AFORO_API_KEY, productType: 'API' }));
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function expressMiddleware(options) {
|
|
16
|
+
const client = new AforoClient({
|
|
17
|
+
apiKey: options.apiKey,
|
|
18
|
+
baseUrl: options.baseUrl,
|
|
19
|
+
...options.clientOptions,
|
|
20
|
+
});
|
|
21
|
+
const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
|
|
22
|
+
const excludeStatusCodes = options.excludeStatusCodes ?? [];
|
|
23
|
+
return function aforoMeteringMiddleware(req, res, next) {
|
|
24
|
+
// Capture timing
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
res.on('finish', () => {
|
|
27
|
+
try {
|
|
28
|
+
const path = req.originalUrl || req.url || '/';
|
|
29
|
+
const method = req.method || 'UNKNOWN';
|
|
30
|
+
const statusCode = res.statusCode || 0;
|
|
31
|
+
// Check exclusions
|
|
32
|
+
if (isPreflight(method))
|
|
33
|
+
return;
|
|
34
|
+
if (excludePaths.some((p) => path.startsWith(p)))
|
|
35
|
+
return;
|
|
36
|
+
if (excludeStatusCodes.includes(statusCode))
|
|
37
|
+
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
|
|
61
|
+
if (!(quantity > 0))
|
|
62
|
+
return; // The ingestor rejects quantity <= 0
|
|
63
|
+
// Build metadata
|
|
64
|
+
let metadata;
|
|
65
|
+
if (options.metadata) {
|
|
66
|
+
metadata = options.metadata(req, res);
|
|
67
|
+
}
|
|
68
|
+
client.track({
|
|
69
|
+
customerId,
|
|
70
|
+
metricName,
|
|
71
|
+
quantity,
|
|
72
|
+
metadata,
|
|
73
|
+
productType: options.productType,
|
|
74
|
+
endpointPath: endpointPathOf(path),
|
|
75
|
+
httpMethod: method,
|
|
76
|
+
statusCode,
|
|
77
|
+
responseTimeMs: Date.now() - startTime,
|
|
78
|
+
}).catch(() => { }); // Fire-and-forget
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Never let metering errors affect the API
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
next();
|
|
85
|
+
};
|
|
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']);
|
|
97
|
+
}
|
|
98
|
+
// Also export as `middleware` for convenience
|
|
99
|
+
export const middleware = expressMiddleware;
|
|
100
|
+
//# 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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"}
|