@ar-agents/mercadopago 0.9.0 → 0.11.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/CHANGELOG.md +42 -0
- package/MIGRATION.md +176 -0
- package/dist/audit-B9Nhj3PH.d.cts +294 -0
- package/dist/audit-B9Nhj3PH.d.ts +294 -0
- package/dist/index.cjs +1362 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +792 -6
- package/dist/index.d.ts +792 -6
- package/dist/index.js +1337 -50
- package/dist/index.js.map +1 -1
- package/dist/otel.cjs +102 -0
- package/dist/otel.cjs.map +1 -0
- package/dist/otel.d.cts +90 -0
- package/dist/otel.d.ts +90 -0
- package/dist/otel.js +100 -0
- package/dist/otel.js.map +1 -0
- package/dist/vercel-kv.cjs +81 -0
- package/dist/vercel-kv.cjs.map +1 -1
- package/dist/vercel-kv.d.cts +38 -2
- package/dist/vercel-kv.d.ts +38 -2
- package/dist/vercel-kv.js +81 -1
- package/dist/vercel-kv.js.map +1 -1
- package/package.json +22 -3
- package/tools.manifest.json +1 -1
- package/dist/state-C6Wzb_XX.d.cts +0 -106
- package/dist/state-C6Wzb_XX.d.ts +0 -106
package/dist/otel.cjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/otel.ts
|
|
4
|
+
var cachedApi = void 0;
|
|
5
|
+
async function loadOtelApi() {
|
|
6
|
+
if (cachedApi !== void 0) return cachedApi;
|
|
7
|
+
try {
|
|
8
|
+
const mod = await import(
|
|
9
|
+
/* @vite-ignore */
|
|
10
|
+
'@opentelemetry/api'
|
|
11
|
+
);
|
|
12
|
+
cachedApi = mod;
|
|
13
|
+
return mod;
|
|
14
|
+
} catch {
|
|
15
|
+
cachedApi = null;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function createOtelHooks(opts = {}) {
|
|
20
|
+
const serviceName = opts.serviceName ?? "ar-agents-mercadopago";
|
|
21
|
+
const version = opts.version ?? "0.10.0";
|
|
22
|
+
const baseAttrs = opts.attributes ?? {};
|
|
23
|
+
let initialized = false;
|
|
24
|
+
let api = null;
|
|
25
|
+
let durationHist = null;
|
|
26
|
+
let requestCounter = null;
|
|
27
|
+
let rateLimitGauge = null;
|
|
28
|
+
const ensureInit = async () => {
|
|
29
|
+
if (initialized) return;
|
|
30
|
+
initialized = true;
|
|
31
|
+
api = await loadOtelApi();
|
|
32
|
+
if (!api) return;
|
|
33
|
+
const meter = api.metrics.getMeter(serviceName, version);
|
|
34
|
+
durationHist = meter.createHistogram("mp.requests.duration", {
|
|
35
|
+
description: "MP API request duration",
|
|
36
|
+
unit: "ms"
|
|
37
|
+
});
|
|
38
|
+
requestCounter = meter.createCounter("mp.requests.count", {
|
|
39
|
+
description: "MP API requests count by outcome"
|
|
40
|
+
});
|
|
41
|
+
rateLimitGauge = meter.createGauge?.("mp.rate_limit.remaining", {
|
|
42
|
+
description: "MP-reported rate limit remaining at last response",
|
|
43
|
+
unit: "1"
|
|
44
|
+
}) ?? null;
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
onCall: (event) => {
|
|
48
|
+
void (async () => {
|
|
49
|
+
await ensureInit();
|
|
50
|
+
if (!api) return;
|
|
51
|
+
const attrs = {
|
|
52
|
+
...baseAttrs,
|
|
53
|
+
"mp.method": event.method,
|
|
54
|
+
"mp.path": event.path,
|
|
55
|
+
"mp.success": event.success,
|
|
56
|
+
"mp.retried": event.retried
|
|
57
|
+
};
|
|
58
|
+
if (event.httpStatus !== null) attrs["http.status_code"] = event.httpStatus;
|
|
59
|
+
if (event.requestId) attrs["mp.request_id"] = event.requestId;
|
|
60
|
+
if (event.circuitState) attrs["mp.circuit_state"] = event.circuitState;
|
|
61
|
+
durationHist?.record(event.durationMs, attrs);
|
|
62
|
+
requestCounter?.add(1, attrs);
|
|
63
|
+
if (event.rateLimit?.remaining !== null && event.rateLimit?.remaining !== void 0) {
|
|
64
|
+
rateLimitGauge?.record(event.rateLimit.remaining, {
|
|
65
|
+
"mp.path": event.path
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const tracer = api.trace.getTracer(serviceName, version);
|
|
69
|
+
const span = tracer.startSpan(`mp.${event.method}.${event.path}`, {
|
|
70
|
+
attributes: attrs
|
|
71
|
+
});
|
|
72
|
+
if (event.success) {
|
|
73
|
+
span.setStatus({ code: api.SpanStatusCode.OK });
|
|
74
|
+
} else {
|
|
75
|
+
span.setStatus({
|
|
76
|
+
code: api.SpanStatusCode.ERROR,
|
|
77
|
+
message: `MP request failed (status=${event.httpStatus})`
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
span.end();
|
|
81
|
+
})();
|
|
82
|
+
},
|
|
83
|
+
traceContext: () => {
|
|
84
|
+
if (!api) {
|
|
85
|
+
if (cachedApi) api = cachedApi;
|
|
86
|
+
}
|
|
87
|
+
if (!api) return void 0;
|
|
88
|
+
const span = api.trace.getActiveSpan();
|
|
89
|
+
if (!span) return void 0;
|
|
90
|
+
const ctx = span.spanContext();
|
|
91
|
+
return {
|
|
92
|
+
traceId: ctx.traceId,
|
|
93
|
+
spanId: ctx.spanId,
|
|
94
|
+
traceFlags: ctx.traceFlags
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
exports.createOtelHooks = createOtelHooks;
|
|
101
|
+
//# sourceMappingURL=otel.cjs.map
|
|
102
|
+
//# sourceMappingURL=otel.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/otel.ts"],"names":[],"mappings":";;;AA2EA,IAAI,SAAA,GAAwC,MAAA;AAE5C,eAAe,WAAA,GAAuC;AACpD,EAAA,IAAI,SAAA,KAAc,QAAW,OAAO,SAAA;AACpC,EAAA,IAAI;AACF,IAAA,MAAM,MAAO,MAAM;AAAA;AAAA,MACE;AAAA,KACrB;AACA,IAAA,SAAA,GAAY,GAAA;AACZ,IAAA,OAAO,GAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,SAAA,GAAY,IAAA;AACZ,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAqBO,SAAS,eAAA,CAAgB,IAAA,GAAyB,EAAC,EAcxD;AACA,EAAA,MAAM,WAAA,GAAc,KAAK,WAAA,IAAe,uBAAA;AACxC,EAAA,MAAM,OAAA,GAAU,KAAK,OAAA,IAAW,QAAA;AAChC,EAAA,MAAM,SAAA,GAAY,IAAA,CAAK,UAAA,IAAc,EAAC;AAGtC,EAAA,IAAI,WAAA,GAAc,KAAA;AAClB,EAAA,IAAI,GAAA,GAAsB,IAAA;AAC1B,EAAA,IAAI,YAAA,GAAiC,IAAA;AACrC,EAAA,IAAI,cAAA,GAAiC,IAAA;AACrC,EAAA,IAAI,cAAA,GAA+B,IAAA;AAEnC,EAAA,MAAM,aAAa,YAAY;AAC7B,IAAA,IAAI,WAAA,EAAa;AACjB,IAAA,WAAA,GAAc,IAAA;AACd,IAAA,GAAA,GAAM,MAAM,WAAA,EAAY;AACxB,IAAA,IAAI,CAAC,GAAA,EAAK;AACV,IAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,OAAA,CAAQ,QAAA,CAAS,aAAa,OAAO,CAAA;AACvD,IAAA,YAAA,GAAe,KAAA,CAAM,gBAAgB,sBAAA,EAAwB;AAAA,MAC3D,WAAA,EAAa,yBAAA;AAAA,MACb,IAAA,EAAM;AAAA,KACP,CAAA;AACD,IAAA,cAAA,GAAiB,KAAA,CAAM,cAAc,mBAAA,EAAqB;AAAA,MACxD,WAAA,EAAa;AAAA,KACd,CAAA;AACD,IAAA,cAAA,GAAiB,KAAA,CAAM,cAAc,yBAAA,EAA2B;AAAA,MAC9D,WAAA,EAAa,mDAAA;AAAA,MACb,IAAA,EAAM;AAAA,KACP,CAAA,IAAK,IAAA;AAAA,EACR,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,CAAC,KAAA,KAAU;AAEjB,MAAA,KAAA,CAAM,YAAY;AAChB,QAAA,MAAM,UAAA,EAAW;AACjB,QAAA,IAAI,CAAC,GAAA,EAAK;AACV,QAAA,MAAM,KAAA,GAAiC;AAAA,UACrC,GAAG,SAAA;AAAA,UACH,aAAa,KAAA,CAAM,MAAA;AAAA,UACnB,WAAW,KAAA,CAAM,IAAA;AAAA,UACjB,cAAc,KAAA,CAAM,OAAA;AAAA,UACpB,cAAc,KAAA,CAAM;AAAA,SACtB;AACA,QAAA,IAAI,MAAM,UAAA,KAAe,IAAA,EAAM,KAAA,CAAM,kBAAkB,IAAI,KAAA,CAAM,UAAA;AACjE,QAAA,IAAI,KAAA,CAAM,SAAA,EAAW,KAAA,CAAM,eAAe,IAAI,KAAA,CAAM,SAAA;AACpD,QAAA,IAAI,KAAA,CAAM,YAAA,EAAc,KAAA,CAAM,kBAAkB,IAAI,KAAA,CAAM,YAAA;AAE1D,QAAA,YAAA,EAAc,MAAA,CAAO,KAAA,CAAM,UAAA,EAAY,KAAK,CAAA;AAC5C,QAAA,cAAA,EAAgB,GAAA,CAAI,GAAG,KAAK,CAAA;AAC5B,QAAA,IAAI,MAAM,SAAA,EAAW,SAAA,KAAc,QAAQ,KAAA,CAAM,SAAA,EAAW,cAAc,MAAA,EAAW;AACnF,UAAA,cAAA,EAAgB,MAAA,CAAO,KAAA,CAAM,SAAA,CAAU,SAAA,EAAW;AAAA,YAChD,WAAW,KAAA,CAAM;AAAA,WAClB,CAAA;AAAA,QACH;AAGA,QAAA,MAAM,MAAA,GAAS,GAAA,CAAI,KAAA,CAAM,SAAA,CAAU,aAAa,OAAO,CAAA;AACvD,QAAA,MAAM,IAAA,GAAO,OAAO,SAAA,CAAU,CAAA,GAAA,EAAM,MAAM,MAAM,CAAA,CAAA,EAAI,KAAA,CAAM,IAAI,CAAA,CAAA,EAAI;AAAA,UAChE,UAAA,EAAY;AAAA,SACb,CAAA;AACD,QAAA,IAAI,MAAM,OAAA,EAAS;AACjB,UAAA,IAAA,CAAK,UAAU,EAAE,IAAA,EAAM,GAAA,CAAI,cAAA,CAAe,IAAI,CAAA;AAAA,QAChD,CAAA,MAAO;AACL,UAAA,IAAA,CAAK,SAAA,CAAU;AAAA,YACb,IAAA,EAAM,IAAI,cAAA,CAAe,KAAA;AAAA,YACzB,OAAA,EAAS,CAAA,0BAAA,EAA6B,KAAA,CAAM,UAAU,CAAA,CAAA;AAAA,WACvD,CAAA;AAAA,QACH;AACA,QAAA,IAAA,CAAK,GAAA,EAAI;AAAA,MACX,CAAA,GAAG;AAAA,IACL,CAAA;AAAA,IAEA,cAAc,MAAM;AAGlB,MAAA,IAAI,CAAC,GAAA,EAAK;AAER,QAAA,IAAI,WAAW,GAAA,GAAM,SAAA;AAAA,MACvB;AACA,MAAA,IAAI,CAAC,KAAK,OAAO,MAAA;AACjB,MAAA,MAAM,IAAA,GAAO,GAAA,CAAI,KAAA,CAAM,aAAA,EAAc;AACrC,MAAA,IAAI,CAAC,MAAM,OAAO,MAAA;AAClB,MAAA,MAAM,GAAA,GAAM,KAAK,WAAA,EAAY;AAC7B,MAAA,OAAO;AAAA,QACL,SAAS,GAAA,CAAI,OAAA;AAAA,QACb,QAAQ,GAAA,CAAI,MAAA;AAAA,QACZ,YAAY,GAAA,CAAI;AAAA,OAClB;AAAA,IACF;AAAA,GACF;AACF","file":"otel.cjs","sourcesContent":["/**\n * OpenTelemetry instrumentation — drop-in tracing + metrics for the MP toolkit.\n *\n * # Why a subpath?\n *\n * `@opentelemetry/api` is an OPTIONAL peer dep. Consumers who don't use\n * OpenTelemetry don't pay the bundle cost. Consumers who DO use it import\n * from `@ar-agents/mercadopago/otel` and get instant instrumentation:\n * spans for every MP request, metrics for latency/errors/rate-limit\n * remaining, and proper context propagation to your downstream traces.\n *\n * # Setup\n *\n * 1. Install: `pnpm add @opentelemetry/api`\n * 2. Wire your tracer + meter at app boot (per OpenTelemetry standard).\n * 3. Pass the instrumented hooks to MercadoPagoClient:\n *\n * ```ts\n * import { MercadoPagoClient } from \"@ar-agents/mercadopago\";\n * import { createOtelHooks } from \"@ar-agents/mercadopago/otel\";\n *\n * const otel = createOtelHooks({ serviceName: \"billing-bot\" });\n * const client = new MercadoPagoClient({\n * accessToken: process.env.MP_ACCESS_TOKEN!,\n * onCall: otel.onCall,\n * traceContext: otel.traceContext,\n * });\n * ```\n *\n * # What gets instrumented\n *\n * - **Spans**: one span per MP request, named `mp.{method}.{path}` (e.g.,\n * `mp.GET./v1/payments/123`). Includes attributes: status code,\n * request_id, retried count, success bool, MP rate-limit remaining,\n * circuit breaker state.\n * - **Metrics**: `mp.requests.duration` histogram (ms), `mp.requests.count`\n * counter (labeled by success/method/path/status), `mp.rate_limit.remaining`\n * gauge.\n *\n * # No-op fallback\n *\n * If `@opentelemetry/api` isn't installed at runtime, the hooks degrade to\n * no-ops gracefully (without throwing) so the toolkit remains importable\n * even without OTEL configured.\n */\n\n// Types-only imports — `@opentelemetry/api` is an OPTIONAL peer dep.\n// We resolve at runtime via dynamic import; if absent, we run as no-ops.\ntype Tracer = {\n startSpan(name: string, options?: unknown): {\n setAttribute(k: string, v: unknown): void;\n setStatus(s: { code: number; message?: string }): void;\n end(): void;\n spanContext(): { traceId: string; spanId: string; traceFlags: number };\n };\n};\ntype Histogram = { record(v: number, attributes?: Record<string, unknown>): void };\ntype Counter = { add(v: number, attributes?: Record<string, unknown>): void };\ntype Gauge = { record(v: number, attributes?: Record<string, unknown>): void };\n\ninterface OtelApi {\n trace: {\n getTracer(name: string, version?: string): Tracer;\n getActiveSpan(): { spanContext(): { traceId: string; spanId: string; traceFlags: number } } | undefined;\n };\n metrics: {\n getMeter(name: string, version?: string): {\n createHistogram(name: string, opts?: { description?: string; unit?: string }): Histogram;\n createCounter(name: string, opts?: { description?: string }): Counter;\n createGauge?(name: string, opts?: { description?: string; unit?: string }): Gauge;\n };\n };\n SpanStatusCode: { OK: number; ERROR: number; UNSET: number };\n}\n\nlet cachedApi: OtelApi | null | undefined = undefined;\n\nasync function loadOtelApi(): Promise<OtelApi | null> {\n if (cachedApi !== undefined) return cachedApi;\n try {\n const mod = (await import(\n /* @vite-ignore */ \"@opentelemetry/api\"\n )) as unknown as OtelApi;\n cachedApi = mod;\n return mod;\n } catch {\n cachedApi = null;\n return null;\n }\n}\n\nexport interface OtelHooksOptions {\n /** Service name shown in trace UIs. Default \"ar-agents-mercadopago\". */\n serviceName?: string;\n /** Toolkit version (defaults to a static \"0.10.x\"). */\n version?: string;\n /**\n * Attributes added to every span/metric (e.g., environment, deployment_id).\n */\n attributes?: Record<string, string | number | boolean>;\n}\n\n/**\n * Build OpenTelemetry-aware hooks for `MercadoPagoClient`. Returns:\n *\n * - `onCall`: wires every request into traces + metrics\n * - `traceContext`: extracts active span context for traceparent propagation\n *\n * Both degrade to no-ops if `@opentelemetry/api` isn't installed.\n */\nexport function createOtelHooks(opts: OtelHooksOptions = {}): {\n onCall: (event: {\n method: string;\n path: string;\n durationMs: number;\n httpStatus: number | null;\n retried: number;\n success: boolean;\n requestId?: string | null;\n rateLimit?: { remaining: number | null; resetSeconds: number | null };\n circuitState?: \"CLOSED\" | \"OPEN\" | \"HALF_OPEN\";\n traceContext?: { traceId?: string; spanId?: string };\n }) => void;\n traceContext: () => { traceId?: string; spanId?: string; traceFlags?: number } | undefined;\n} {\n const serviceName = opts.serviceName ?? \"ar-agents-mercadopago\";\n const version = opts.version ?? \"0.10.0\";\n const baseAttrs = opts.attributes ?? {};\n\n // Lazy state — resolved on first invocation (so module load is sync + safe).\n let initialized = false;\n let api: OtelApi | null = null;\n let durationHist: Histogram | null = null;\n let requestCounter: Counter | null = null;\n let rateLimitGauge: Gauge | null = null;\n\n const ensureInit = async () => {\n if (initialized) return;\n initialized = true;\n api = await loadOtelApi();\n if (!api) return;\n const meter = api.metrics.getMeter(serviceName, version);\n durationHist = meter.createHistogram(\"mp.requests.duration\", {\n description: \"MP API request duration\",\n unit: \"ms\",\n });\n requestCounter = meter.createCounter(\"mp.requests.count\", {\n description: \"MP API requests count by outcome\",\n });\n rateLimitGauge = meter.createGauge?.(\"mp.rate_limit.remaining\", {\n description: \"MP-reported rate limit remaining at last response\",\n unit: \"1\",\n }) ?? null;\n };\n\n return {\n onCall: (event) => {\n // Don't await — fire-and-forget. Best-effort observability.\n void (async () => {\n await ensureInit();\n if (!api) return;\n const attrs: Record<string, unknown> = {\n ...baseAttrs,\n \"mp.method\": event.method,\n \"mp.path\": event.path,\n \"mp.success\": event.success,\n \"mp.retried\": event.retried,\n };\n if (event.httpStatus !== null) attrs[\"http.status_code\"] = event.httpStatus;\n if (event.requestId) attrs[\"mp.request_id\"] = event.requestId;\n if (event.circuitState) attrs[\"mp.circuit_state\"] = event.circuitState;\n\n durationHist?.record(event.durationMs, attrs);\n requestCounter?.add(1, attrs);\n if (event.rateLimit?.remaining !== null && event.rateLimit?.remaining !== undefined) {\n rateLimitGauge?.record(event.rateLimit.remaining, {\n \"mp.path\": event.path,\n });\n }\n\n // Span: emit a synthetic child span scoped to this request's duration.\n const tracer = api.trace.getTracer(serviceName, version);\n const span = tracer.startSpan(`mp.${event.method}.${event.path}`, {\n attributes: attrs,\n });\n if (event.success) {\n span.setStatus({ code: api.SpanStatusCode.OK });\n } else {\n span.setStatus({\n code: api.SpanStatusCode.ERROR,\n message: `MP request failed (status=${event.httpStatus})`,\n });\n }\n span.end();\n })();\n },\n\n traceContext: () => {\n // Synchronous getter — must be cheap. Resolves the OTEL active span\n // context if available; returns undefined if OTEL isn't installed.\n if (!api) {\n // Try to use the cached api if loadOtelApi already ran\n if (cachedApi) api = cachedApi;\n }\n if (!api) return undefined;\n const span = api.trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n return {\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n traceFlags: ctx.traceFlags,\n };\n },\n };\n}\n"]}
|
package/dist/otel.d.cts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry instrumentation — drop-in tracing + metrics for the MP toolkit.
|
|
3
|
+
*
|
|
4
|
+
* # Why a subpath?
|
|
5
|
+
*
|
|
6
|
+
* `@opentelemetry/api` is an OPTIONAL peer dep. Consumers who don't use
|
|
7
|
+
* OpenTelemetry don't pay the bundle cost. Consumers who DO use it import
|
|
8
|
+
* from `@ar-agents/mercadopago/otel` and get instant instrumentation:
|
|
9
|
+
* spans for every MP request, metrics for latency/errors/rate-limit
|
|
10
|
+
* remaining, and proper context propagation to your downstream traces.
|
|
11
|
+
*
|
|
12
|
+
* # Setup
|
|
13
|
+
*
|
|
14
|
+
* 1. Install: `pnpm add @opentelemetry/api`
|
|
15
|
+
* 2. Wire your tracer + meter at app boot (per OpenTelemetry standard).
|
|
16
|
+
* 3. Pass the instrumented hooks to MercadoPagoClient:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { MercadoPagoClient } from "@ar-agents/mercadopago";
|
|
20
|
+
* import { createOtelHooks } from "@ar-agents/mercadopago/otel";
|
|
21
|
+
*
|
|
22
|
+
* const otel = createOtelHooks({ serviceName: "billing-bot" });
|
|
23
|
+
* const client = new MercadoPagoClient({
|
|
24
|
+
* accessToken: process.env.MP_ACCESS_TOKEN!,
|
|
25
|
+
* onCall: otel.onCall,
|
|
26
|
+
* traceContext: otel.traceContext,
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* # What gets instrumented
|
|
31
|
+
*
|
|
32
|
+
* - **Spans**: one span per MP request, named `mp.{method}.{path}` (e.g.,
|
|
33
|
+
* `mp.GET./v1/payments/123`). Includes attributes: status code,
|
|
34
|
+
* request_id, retried count, success bool, MP rate-limit remaining,
|
|
35
|
+
* circuit breaker state.
|
|
36
|
+
* - **Metrics**: `mp.requests.duration` histogram (ms), `mp.requests.count`
|
|
37
|
+
* counter (labeled by success/method/path/status), `mp.rate_limit.remaining`
|
|
38
|
+
* gauge.
|
|
39
|
+
*
|
|
40
|
+
* # No-op fallback
|
|
41
|
+
*
|
|
42
|
+
* If `@opentelemetry/api` isn't installed at runtime, the hooks degrade to
|
|
43
|
+
* no-ops gracefully (without throwing) so the toolkit remains importable
|
|
44
|
+
* even without OTEL configured.
|
|
45
|
+
*/
|
|
46
|
+
interface OtelHooksOptions {
|
|
47
|
+
/** Service name shown in trace UIs. Default "ar-agents-mercadopago". */
|
|
48
|
+
serviceName?: string;
|
|
49
|
+
/** Toolkit version (defaults to a static "0.10.x"). */
|
|
50
|
+
version?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Attributes added to every span/metric (e.g., environment, deployment_id).
|
|
53
|
+
*/
|
|
54
|
+
attributes?: Record<string, string | number | boolean>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build OpenTelemetry-aware hooks for `MercadoPagoClient`. Returns:
|
|
58
|
+
*
|
|
59
|
+
* - `onCall`: wires every request into traces + metrics
|
|
60
|
+
* - `traceContext`: extracts active span context for traceparent propagation
|
|
61
|
+
*
|
|
62
|
+
* Both degrade to no-ops if `@opentelemetry/api` isn't installed.
|
|
63
|
+
*/
|
|
64
|
+
declare function createOtelHooks(opts?: OtelHooksOptions): {
|
|
65
|
+
onCall: (event: {
|
|
66
|
+
method: string;
|
|
67
|
+
path: string;
|
|
68
|
+
durationMs: number;
|
|
69
|
+
httpStatus: number | null;
|
|
70
|
+
retried: number;
|
|
71
|
+
success: boolean;
|
|
72
|
+
requestId?: string | null;
|
|
73
|
+
rateLimit?: {
|
|
74
|
+
remaining: number | null;
|
|
75
|
+
resetSeconds: number | null;
|
|
76
|
+
};
|
|
77
|
+
circuitState?: "CLOSED" | "OPEN" | "HALF_OPEN";
|
|
78
|
+
traceContext?: {
|
|
79
|
+
traceId?: string;
|
|
80
|
+
spanId?: string;
|
|
81
|
+
};
|
|
82
|
+
}) => void;
|
|
83
|
+
traceContext: () => {
|
|
84
|
+
traceId?: string;
|
|
85
|
+
spanId?: string;
|
|
86
|
+
traceFlags?: number;
|
|
87
|
+
} | undefined;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export { type OtelHooksOptions, createOtelHooks };
|
package/dist/otel.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry instrumentation — drop-in tracing + metrics for the MP toolkit.
|
|
3
|
+
*
|
|
4
|
+
* # Why a subpath?
|
|
5
|
+
*
|
|
6
|
+
* `@opentelemetry/api` is an OPTIONAL peer dep. Consumers who don't use
|
|
7
|
+
* OpenTelemetry don't pay the bundle cost. Consumers who DO use it import
|
|
8
|
+
* from `@ar-agents/mercadopago/otel` and get instant instrumentation:
|
|
9
|
+
* spans for every MP request, metrics for latency/errors/rate-limit
|
|
10
|
+
* remaining, and proper context propagation to your downstream traces.
|
|
11
|
+
*
|
|
12
|
+
* # Setup
|
|
13
|
+
*
|
|
14
|
+
* 1. Install: `pnpm add @opentelemetry/api`
|
|
15
|
+
* 2. Wire your tracer + meter at app boot (per OpenTelemetry standard).
|
|
16
|
+
* 3. Pass the instrumented hooks to MercadoPagoClient:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { MercadoPagoClient } from "@ar-agents/mercadopago";
|
|
20
|
+
* import { createOtelHooks } from "@ar-agents/mercadopago/otel";
|
|
21
|
+
*
|
|
22
|
+
* const otel = createOtelHooks({ serviceName: "billing-bot" });
|
|
23
|
+
* const client = new MercadoPagoClient({
|
|
24
|
+
* accessToken: process.env.MP_ACCESS_TOKEN!,
|
|
25
|
+
* onCall: otel.onCall,
|
|
26
|
+
* traceContext: otel.traceContext,
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* # What gets instrumented
|
|
31
|
+
*
|
|
32
|
+
* - **Spans**: one span per MP request, named `mp.{method}.{path}` (e.g.,
|
|
33
|
+
* `mp.GET./v1/payments/123`). Includes attributes: status code,
|
|
34
|
+
* request_id, retried count, success bool, MP rate-limit remaining,
|
|
35
|
+
* circuit breaker state.
|
|
36
|
+
* - **Metrics**: `mp.requests.duration` histogram (ms), `mp.requests.count`
|
|
37
|
+
* counter (labeled by success/method/path/status), `mp.rate_limit.remaining`
|
|
38
|
+
* gauge.
|
|
39
|
+
*
|
|
40
|
+
* # No-op fallback
|
|
41
|
+
*
|
|
42
|
+
* If `@opentelemetry/api` isn't installed at runtime, the hooks degrade to
|
|
43
|
+
* no-ops gracefully (without throwing) so the toolkit remains importable
|
|
44
|
+
* even without OTEL configured.
|
|
45
|
+
*/
|
|
46
|
+
interface OtelHooksOptions {
|
|
47
|
+
/** Service name shown in trace UIs. Default "ar-agents-mercadopago". */
|
|
48
|
+
serviceName?: string;
|
|
49
|
+
/** Toolkit version (defaults to a static "0.10.x"). */
|
|
50
|
+
version?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Attributes added to every span/metric (e.g., environment, deployment_id).
|
|
53
|
+
*/
|
|
54
|
+
attributes?: Record<string, string | number | boolean>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build OpenTelemetry-aware hooks for `MercadoPagoClient`. Returns:
|
|
58
|
+
*
|
|
59
|
+
* - `onCall`: wires every request into traces + metrics
|
|
60
|
+
* - `traceContext`: extracts active span context for traceparent propagation
|
|
61
|
+
*
|
|
62
|
+
* Both degrade to no-ops if `@opentelemetry/api` isn't installed.
|
|
63
|
+
*/
|
|
64
|
+
declare function createOtelHooks(opts?: OtelHooksOptions): {
|
|
65
|
+
onCall: (event: {
|
|
66
|
+
method: string;
|
|
67
|
+
path: string;
|
|
68
|
+
durationMs: number;
|
|
69
|
+
httpStatus: number | null;
|
|
70
|
+
retried: number;
|
|
71
|
+
success: boolean;
|
|
72
|
+
requestId?: string | null;
|
|
73
|
+
rateLimit?: {
|
|
74
|
+
remaining: number | null;
|
|
75
|
+
resetSeconds: number | null;
|
|
76
|
+
};
|
|
77
|
+
circuitState?: "CLOSED" | "OPEN" | "HALF_OPEN";
|
|
78
|
+
traceContext?: {
|
|
79
|
+
traceId?: string;
|
|
80
|
+
spanId?: string;
|
|
81
|
+
};
|
|
82
|
+
}) => void;
|
|
83
|
+
traceContext: () => {
|
|
84
|
+
traceId?: string;
|
|
85
|
+
spanId?: string;
|
|
86
|
+
traceFlags?: number;
|
|
87
|
+
} | undefined;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export { type OtelHooksOptions, createOtelHooks };
|
package/dist/otel.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/otel.ts
|
|
2
|
+
var cachedApi = void 0;
|
|
3
|
+
async function loadOtelApi() {
|
|
4
|
+
if (cachedApi !== void 0) return cachedApi;
|
|
5
|
+
try {
|
|
6
|
+
const mod = await import(
|
|
7
|
+
/* @vite-ignore */
|
|
8
|
+
'@opentelemetry/api'
|
|
9
|
+
);
|
|
10
|
+
cachedApi = mod;
|
|
11
|
+
return mod;
|
|
12
|
+
} catch {
|
|
13
|
+
cachedApi = null;
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function createOtelHooks(opts = {}) {
|
|
18
|
+
const serviceName = opts.serviceName ?? "ar-agents-mercadopago";
|
|
19
|
+
const version = opts.version ?? "0.10.0";
|
|
20
|
+
const baseAttrs = opts.attributes ?? {};
|
|
21
|
+
let initialized = false;
|
|
22
|
+
let api = null;
|
|
23
|
+
let durationHist = null;
|
|
24
|
+
let requestCounter = null;
|
|
25
|
+
let rateLimitGauge = null;
|
|
26
|
+
const ensureInit = async () => {
|
|
27
|
+
if (initialized) return;
|
|
28
|
+
initialized = true;
|
|
29
|
+
api = await loadOtelApi();
|
|
30
|
+
if (!api) return;
|
|
31
|
+
const meter = api.metrics.getMeter(serviceName, version);
|
|
32
|
+
durationHist = meter.createHistogram("mp.requests.duration", {
|
|
33
|
+
description: "MP API request duration",
|
|
34
|
+
unit: "ms"
|
|
35
|
+
});
|
|
36
|
+
requestCounter = meter.createCounter("mp.requests.count", {
|
|
37
|
+
description: "MP API requests count by outcome"
|
|
38
|
+
});
|
|
39
|
+
rateLimitGauge = meter.createGauge?.("mp.rate_limit.remaining", {
|
|
40
|
+
description: "MP-reported rate limit remaining at last response",
|
|
41
|
+
unit: "1"
|
|
42
|
+
}) ?? null;
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
onCall: (event) => {
|
|
46
|
+
void (async () => {
|
|
47
|
+
await ensureInit();
|
|
48
|
+
if (!api) return;
|
|
49
|
+
const attrs = {
|
|
50
|
+
...baseAttrs,
|
|
51
|
+
"mp.method": event.method,
|
|
52
|
+
"mp.path": event.path,
|
|
53
|
+
"mp.success": event.success,
|
|
54
|
+
"mp.retried": event.retried
|
|
55
|
+
};
|
|
56
|
+
if (event.httpStatus !== null) attrs["http.status_code"] = event.httpStatus;
|
|
57
|
+
if (event.requestId) attrs["mp.request_id"] = event.requestId;
|
|
58
|
+
if (event.circuitState) attrs["mp.circuit_state"] = event.circuitState;
|
|
59
|
+
durationHist?.record(event.durationMs, attrs);
|
|
60
|
+
requestCounter?.add(1, attrs);
|
|
61
|
+
if (event.rateLimit?.remaining !== null && event.rateLimit?.remaining !== void 0) {
|
|
62
|
+
rateLimitGauge?.record(event.rateLimit.remaining, {
|
|
63
|
+
"mp.path": event.path
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const tracer = api.trace.getTracer(serviceName, version);
|
|
67
|
+
const span = tracer.startSpan(`mp.${event.method}.${event.path}`, {
|
|
68
|
+
attributes: attrs
|
|
69
|
+
});
|
|
70
|
+
if (event.success) {
|
|
71
|
+
span.setStatus({ code: api.SpanStatusCode.OK });
|
|
72
|
+
} else {
|
|
73
|
+
span.setStatus({
|
|
74
|
+
code: api.SpanStatusCode.ERROR,
|
|
75
|
+
message: `MP request failed (status=${event.httpStatus})`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
span.end();
|
|
79
|
+
})();
|
|
80
|
+
},
|
|
81
|
+
traceContext: () => {
|
|
82
|
+
if (!api) {
|
|
83
|
+
if (cachedApi) api = cachedApi;
|
|
84
|
+
}
|
|
85
|
+
if (!api) return void 0;
|
|
86
|
+
const span = api.trace.getActiveSpan();
|
|
87
|
+
if (!span) return void 0;
|
|
88
|
+
const ctx = span.spanContext();
|
|
89
|
+
return {
|
|
90
|
+
traceId: ctx.traceId,
|
|
91
|
+
spanId: ctx.spanId,
|
|
92
|
+
traceFlags: ctx.traceFlags
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { createOtelHooks };
|
|
99
|
+
//# sourceMappingURL=otel.js.map
|
|
100
|
+
//# sourceMappingURL=otel.js.map
|
package/dist/otel.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/otel.ts"],"names":[],"mappings":"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","file":"otel.js","sourcesContent":["/**\n * OpenTelemetry instrumentation — drop-in tracing + metrics for the MP toolkit.\n *\n * # Why a subpath?\n *\n * `@opentelemetry/api` is an OPTIONAL peer dep. Consumers who don't use\n * OpenTelemetry don't pay the bundle cost. Consumers who DO use it import\n * from `@ar-agents/mercadopago/otel` and get instant instrumentation:\n * spans for every MP request, metrics for latency/errors/rate-limit\n * remaining, and proper context propagation to your downstream traces.\n *\n * # Setup\n *\n * 1. Install: `pnpm add @opentelemetry/api`\n * 2. Wire your tracer + meter at app boot (per OpenTelemetry standard).\n * 3. Pass the instrumented hooks to MercadoPagoClient:\n *\n * ```ts\n * import { MercadoPagoClient } from \"@ar-agents/mercadopago\";\n * import { createOtelHooks } from \"@ar-agents/mercadopago/otel\";\n *\n * const otel = createOtelHooks({ serviceName: \"billing-bot\" });\n * const client = new MercadoPagoClient({\n * accessToken: process.env.MP_ACCESS_TOKEN!,\n * onCall: otel.onCall,\n * traceContext: otel.traceContext,\n * });\n * ```\n *\n * # What gets instrumented\n *\n * - **Spans**: one span per MP request, named `mp.{method}.{path}` (e.g.,\n * `mp.GET./v1/payments/123`). Includes attributes: status code,\n * request_id, retried count, success bool, MP rate-limit remaining,\n * circuit breaker state.\n * - **Metrics**: `mp.requests.duration` histogram (ms), `mp.requests.count`\n * counter (labeled by success/method/path/status), `mp.rate_limit.remaining`\n * gauge.\n *\n * # No-op fallback\n *\n * If `@opentelemetry/api` isn't installed at runtime, the hooks degrade to\n * no-ops gracefully (without throwing) so the toolkit remains importable\n * even without OTEL configured.\n */\n\n// Types-only imports — `@opentelemetry/api` is an OPTIONAL peer dep.\n// We resolve at runtime via dynamic import; if absent, we run as no-ops.\ntype Tracer = {\n startSpan(name: string, options?: unknown): {\n setAttribute(k: string, v: unknown): void;\n setStatus(s: { code: number; message?: string }): void;\n end(): void;\n spanContext(): { traceId: string; spanId: string; traceFlags: number };\n };\n};\ntype Histogram = { record(v: number, attributes?: Record<string, unknown>): void };\ntype Counter = { add(v: number, attributes?: Record<string, unknown>): void };\ntype Gauge = { record(v: number, attributes?: Record<string, unknown>): void };\n\ninterface OtelApi {\n trace: {\n getTracer(name: string, version?: string): Tracer;\n getActiveSpan(): { spanContext(): { traceId: string; spanId: string; traceFlags: number } } | undefined;\n };\n metrics: {\n getMeter(name: string, version?: string): {\n createHistogram(name: string, opts?: { description?: string; unit?: string }): Histogram;\n createCounter(name: string, opts?: { description?: string }): Counter;\n createGauge?(name: string, opts?: { description?: string; unit?: string }): Gauge;\n };\n };\n SpanStatusCode: { OK: number; ERROR: number; UNSET: number };\n}\n\nlet cachedApi: OtelApi | null | undefined = undefined;\n\nasync function loadOtelApi(): Promise<OtelApi | null> {\n if (cachedApi !== undefined) return cachedApi;\n try {\n const mod = (await import(\n /* @vite-ignore */ \"@opentelemetry/api\"\n )) as unknown as OtelApi;\n cachedApi = mod;\n return mod;\n } catch {\n cachedApi = null;\n return null;\n }\n}\n\nexport interface OtelHooksOptions {\n /** Service name shown in trace UIs. Default \"ar-agents-mercadopago\". */\n serviceName?: string;\n /** Toolkit version (defaults to a static \"0.10.x\"). */\n version?: string;\n /**\n * Attributes added to every span/metric (e.g., environment, deployment_id).\n */\n attributes?: Record<string, string | number | boolean>;\n}\n\n/**\n * Build OpenTelemetry-aware hooks for `MercadoPagoClient`. Returns:\n *\n * - `onCall`: wires every request into traces + metrics\n * - `traceContext`: extracts active span context for traceparent propagation\n *\n * Both degrade to no-ops if `@opentelemetry/api` isn't installed.\n */\nexport function createOtelHooks(opts: OtelHooksOptions = {}): {\n onCall: (event: {\n method: string;\n path: string;\n durationMs: number;\n httpStatus: number | null;\n retried: number;\n success: boolean;\n requestId?: string | null;\n rateLimit?: { remaining: number | null; resetSeconds: number | null };\n circuitState?: \"CLOSED\" | \"OPEN\" | \"HALF_OPEN\";\n traceContext?: { traceId?: string; spanId?: string };\n }) => void;\n traceContext: () => { traceId?: string; spanId?: string; traceFlags?: number } | undefined;\n} {\n const serviceName = opts.serviceName ?? \"ar-agents-mercadopago\";\n const version = opts.version ?? \"0.10.0\";\n const baseAttrs = opts.attributes ?? {};\n\n // Lazy state — resolved on first invocation (so module load is sync + safe).\n let initialized = false;\n let api: OtelApi | null = null;\n let durationHist: Histogram | null = null;\n let requestCounter: Counter | null = null;\n let rateLimitGauge: Gauge | null = null;\n\n const ensureInit = async () => {\n if (initialized) return;\n initialized = true;\n api = await loadOtelApi();\n if (!api) return;\n const meter = api.metrics.getMeter(serviceName, version);\n durationHist = meter.createHistogram(\"mp.requests.duration\", {\n description: \"MP API request duration\",\n unit: \"ms\",\n });\n requestCounter = meter.createCounter(\"mp.requests.count\", {\n description: \"MP API requests count by outcome\",\n });\n rateLimitGauge = meter.createGauge?.(\"mp.rate_limit.remaining\", {\n description: \"MP-reported rate limit remaining at last response\",\n unit: \"1\",\n }) ?? null;\n };\n\n return {\n onCall: (event) => {\n // Don't await — fire-and-forget. Best-effort observability.\n void (async () => {\n await ensureInit();\n if (!api) return;\n const attrs: Record<string, unknown> = {\n ...baseAttrs,\n \"mp.method\": event.method,\n \"mp.path\": event.path,\n \"mp.success\": event.success,\n \"mp.retried\": event.retried,\n };\n if (event.httpStatus !== null) attrs[\"http.status_code\"] = event.httpStatus;\n if (event.requestId) attrs[\"mp.request_id\"] = event.requestId;\n if (event.circuitState) attrs[\"mp.circuit_state\"] = event.circuitState;\n\n durationHist?.record(event.durationMs, attrs);\n requestCounter?.add(1, attrs);\n if (event.rateLimit?.remaining !== null && event.rateLimit?.remaining !== undefined) {\n rateLimitGauge?.record(event.rateLimit.remaining, {\n \"mp.path\": event.path,\n });\n }\n\n // Span: emit a synthetic child span scoped to this request's duration.\n const tracer = api.trace.getTracer(serviceName, version);\n const span = tracer.startSpan(`mp.${event.method}.${event.path}`, {\n attributes: attrs,\n });\n if (event.success) {\n span.setStatus({ code: api.SpanStatusCode.OK });\n } else {\n span.setStatus({\n code: api.SpanStatusCode.ERROR,\n message: `MP request failed (status=${event.httpStatus})`,\n });\n }\n span.end();\n })();\n },\n\n traceContext: () => {\n // Synchronous getter — must be cheap. Resolves the OTEL active span\n // context if available; returns undefined if OTEL isn't installed.\n if (!api) {\n // Try to use the cached api if loadOtelApi already ran\n if (cachedApi) api = cachedApi;\n }\n if (!api) return undefined;\n const span = api.trace.getActiveSpan();\n if (!span) return undefined;\n const ctx = span.spanContext();\n return {\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n traceFlags: ctx.traceFlags,\n };\n },\n };\n}\n"]}
|
package/dist/vercel-kv.cjs
CHANGED
|
@@ -6,6 +6,7 @@ var kv = require('@vercel/kv');
|
|
|
6
6
|
var DEFAULT_SUBSCRIPTION_PREFIX = "mp:sub:";
|
|
7
7
|
var DEFAULT_OAUTH_PREFIX = "mp:oauth:";
|
|
8
8
|
var DEFAULT_IDEMPOTENCY_PREFIX = "mp:idem:";
|
|
9
|
+
var DEFAULT_AUDIT_PREFIX = "mp:audit:";
|
|
9
10
|
var VercelKVSubscriptionStateAdapter = class {
|
|
10
11
|
kv;
|
|
11
12
|
prefix;
|
|
@@ -84,7 +85,87 @@ var VercelKVIdempotencyCache = class {
|
|
|
84
85
|
await this.kv.del(this.key(key));
|
|
85
86
|
}
|
|
86
87
|
};
|
|
88
|
+
var VercelKVAuditLog = class {
|
|
89
|
+
kv;
|
|
90
|
+
prefix;
|
|
91
|
+
constructor(options = {}) {
|
|
92
|
+
this.kv = options.kv ?? kv.kv;
|
|
93
|
+
this.prefix = options.prefix ?? DEFAULT_AUDIT_PREFIX;
|
|
94
|
+
}
|
|
95
|
+
async append(entry) {
|
|
96
|
+
const ts = new Date(entry.timestamp).getTime();
|
|
97
|
+
const day = entry.timestamp.slice(0, 10);
|
|
98
|
+
await Promise.all([
|
|
99
|
+
this.kv.set(`${this.prefix}entry:${entry.id}`, entry),
|
|
100
|
+
this.kv.zadd(`${this.prefix}day:${day}`, { score: ts, member: entry.id }),
|
|
101
|
+
this.kv.zadd(`${this.prefix}actor:${entry.actor}`, { score: ts, member: entry.id }),
|
|
102
|
+
...entry.tenantId ? [
|
|
103
|
+
this.kv.zadd(`${this.prefix}tenant:${entry.tenantId}`, {
|
|
104
|
+
score: ts,
|
|
105
|
+
member: entry.id
|
|
106
|
+
})
|
|
107
|
+
] : []
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
async query(filter) {
|
|
111
|
+
const limit = filter.limit ?? 100;
|
|
112
|
+
let ids;
|
|
113
|
+
if (filter.actor) {
|
|
114
|
+
ids = await this.zrangeByScore(
|
|
115
|
+
`${this.prefix}actor:${filter.actor}`,
|
|
116
|
+
filter.from,
|
|
117
|
+
filter.to,
|
|
118
|
+
limit
|
|
119
|
+
);
|
|
120
|
+
} else if (filter.tenantId) {
|
|
121
|
+
ids = await this.zrangeByScore(
|
|
122
|
+
`${this.prefix}tenant:${filter.tenantId}`,
|
|
123
|
+
filter.from,
|
|
124
|
+
filter.to,
|
|
125
|
+
limit
|
|
126
|
+
);
|
|
127
|
+
} else if (filter.from || filter.to) {
|
|
128
|
+
const fromDate = filter.from?.slice(0, 10) ?? "0000-00-00";
|
|
129
|
+
const toDate = filter.to?.slice(0, 10) ?? "9999-99-99";
|
|
130
|
+
ids = [];
|
|
131
|
+
const fromTs = new Date(fromDate).getTime();
|
|
132
|
+
const toTs = new Date(toDate).getTime();
|
|
133
|
+
for (let d = fromTs; d <= toTs && ids.length < limit; d += 864e5) {
|
|
134
|
+
const day = new Date(d).toISOString().slice(0, 10);
|
|
135
|
+
const dayIds = await this.zrangeByScore(
|
|
136
|
+
`${this.prefix}day:${day}`,
|
|
137
|
+
filter.from,
|
|
138
|
+
filter.to,
|
|
139
|
+
limit - ids.length
|
|
140
|
+
);
|
|
141
|
+
ids.push(...dayIds);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
const entries = [];
|
|
147
|
+
for (const id of ids) {
|
|
148
|
+
const entry = await this.kv.get(`${this.prefix}entry:${id}`);
|
|
149
|
+
if (!entry) continue;
|
|
150
|
+
if (filter.operation && entry.operation !== filter.operation) continue;
|
|
151
|
+
entries.push(entry);
|
|
152
|
+
}
|
|
153
|
+
return entries;
|
|
154
|
+
}
|
|
155
|
+
async zrangeByScore(key, from, to, limit) {
|
|
156
|
+
const min = from ? new Date(from).getTime() : 0;
|
|
157
|
+
const max = to ? new Date(to).getTime() : Number.MAX_SAFE_INTEGER;
|
|
158
|
+
const opts = {
|
|
159
|
+
byScore: true,
|
|
160
|
+
offset: 0,
|
|
161
|
+
...limit !== void 0 ? { count: limit } : { count: 100 }
|
|
162
|
+
};
|
|
163
|
+
const ids = await this.kv.zrange(key, min, max, opts);
|
|
164
|
+
return ids.map(String);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
87
167
|
|
|
168
|
+
exports.VercelKVAuditLog = VercelKVAuditLog;
|
|
88
169
|
exports.VercelKVIdempotencyCache = VercelKVIdempotencyCache;
|
|
89
170
|
exports.VercelKVOAuthTokenStore = VercelKVOAuthTokenStore;
|
|
90
171
|
exports.VercelKVSubscriptionStateAdapter = VercelKVSubscriptionStateAdapter;
|
package/dist/vercel-kv.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/vercel-kv.ts"],"names":["defaultKv"],"mappings":"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","file":"vercel-kv.cjs","sourcesContent":["/**\n * Vercel KV adapters — drop-in `SubscriptionStateAdapter`,\n * `OAuthTokenStore`, and `IdempotencyCache` implementations backed by\n * [Vercel KV](https://vercel.com/docs/storage/vercel-kv) (Upstash Redis).\n *\n * # Why a separate subpath?\n *\n * `@vercel/kv` is a peer dependency — only consumers who actually use Vercel\n * KV install it. Importing from `@ar-agents/mercadopago/vercel-kv` is\n * lazy: the main `@ar-agents/mercadopago` bundle stays tiny for callers who\n * use the in-memory adapters or a different store.\n *\n * # Setup\n *\n * 1. Create a KV store at https://vercel.com/dashboard/stores\n * 2. Connect it to your project — Vercel auto-injects `KV_*` env vars\n * 3. `pnpm add @vercel/kv`\n * 4. Wire the adapters:\n *\n * ```ts\n * import { mercadoPagoTools, MercadoPagoClient } from \"@ar-agents/mercadopago\";\n * import {\n * VercelKVSubscriptionStateAdapter,\n * VercelKVOAuthTokenStore,\n * } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * const tools = mercadoPagoTools(client, {\n * state: new VercelKVSubscriptionStateAdapter(),\n * backUrl: \"https://mysite.com/done\",\n * // ... oauth, webhookSecret, etc.\n * });\n *\n * // For marketplace flows, also wire the OAuth token store:\n * const oauthStore = new VercelKVOAuthTokenStore();\n * await oauthStore.set(token.user_id, {\n * user_id: token.user_id,\n * access_token: token.access_token,\n * refresh_token: token.refresh_token!,\n * expires_at: Date.now() + (token.expires_in ?? 21600) * 1000,\n * });\n * ```\n *\n * # Edge Runtime\n *\n * `@vercel/kv` works in Vercel Edge Runtime, Node.js, and any environment\n * with `fetch` (it's a thin REST client over Upstash). All adapters here\n * are async and Edge-safe.\n *\n * # Key namespacing\n *\n * Each adapter uses its own prefix so multiple adapters can share the same\n * KV store without collisions:\n * - Subscriptions: `mp:sub:{id}`\n * - OAuth tokens: `mp:oauth:{userId}`\n * - Idempotency: `mp:idem:{key}`\n *\n * Pass a custom prefix via the constructor if you need to share the store\n * with other apps.\n */\n\nimport { kv as defaultKv } from \"@vercel/kv\";\nimport type { VercelKV } from \"@vercel/kv\";\nimport type {\n IdempotencyCache,\n OAuthTokenRecord,\n OAuthTokenStore,\n SubscriptionStateAdapter,\n SubscriptionStateRecord,\n} from \"./state\";\n\nconst DEFAULT_SUBSCRIPTION_PREFIX = \"mp:sub:\";\nconst DEFAULT_OAUTH_PREFIX = \"mp:oauth:\";\nconst DEFAULT_IDEMPOTENCY_PREFIX = \"mp:idem:\";\n\ninterface VercelKVAdapterOptions {\n /**\n * Custom KV client. If omitted, uses the default `kv` export from\n * `@vercel/kv` (which reads `KV_REST_API_URL` + `KV_REST_API_TOKEN` from\n * env — auto-injected when you connect a KV store to your Vercel project).\n */\n kv?: VercelKV;\n /** Override the key prefix. */\n prefix?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SubscriptionStateAdapter\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVSubscriptionStateAdapter\n implements SubscriptionStateAdapter\n{\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_SUBSCRIPTION_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(id: string): string {\n return `${this.prefix}${id}`;\n }\n\n async set(\n id: string,\n state: Partial<SubscriptionStateRecord>,\n ): Promise<void> {\n const existing = (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? {};\n await this.kv.set(this.key(id), { ...existing, ...state });\n await this.kv.sadd(this.indexKey, id);\n }\n\n async get(id: string): Promise<SubscriptionStateRecord | null> {\n return (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? null;\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n\n /** Forget a subscription record. NOT part of the adapter interface. */\n async delete(id: string): Promise<void> {\n await this.kv.del(this.key(id));\n await this.kv.srem(this.indexKey, id);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// OAuthTokenStore (per-seller marketplace token persistence)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVOAuthTokenStore implements OAuthTokenStore {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_OAUTH_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(userId: string): string {\n return `${this.prefix}${userId}`;\n }\n\n async set(userId: string, token: OAuthTokenRecord): Promise<void> {\n await this.kv.set(this.key(userId), token);\n await this.kv.sadd(this.indexKey, userId);\n }\n\n async get(userId: string): Promise<OAuthTokenRecord | null> {\n return (await this.kv.get<OAuthTokenRecord>(this.key(userId))) ?? null;\n }\n\n async delete(userId: string): Promise<void> {\n await this.kv.del(this.key(userId));\n await this.kv.srem(this.indexKey, userId);\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// IdempotencyCache (KV-backed dedup of agent retries)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVIdempotencyCache implements IdempotencyCache {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_IDEMPOTENCY_PREFIX;\n }\n\n private key(k: string): string {\n return `${this.prefix}${k}`;\n }\n\n async get<T>(key: string): Promise<T | null> {\n return (await this.kv.get<T>(this.key(key))) ?? null;\n }\n\n async set<T>(key: string, value: T, ttlSeconds = 86_400): Promise<void> {\n // Vercel KV's `set` supports a TTL in seconds via the `ex` option.\n await this.kv.set(this.key(key), value, { ex: ttlSeconds });\n }\n\n async delete(key: string): Promise<void> {\n await this.kv.del(this.key(key));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/vercel-kv.ts"],"names":["defaultKv"],"mappings":"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","file":"vercel-kv.cjs","sourcesContent":["/**\n * Vercel KV adapters — drop-in `SubscriptionStateAdapter`,\n * `OAuthTokenStore`, and `IdempotencyCache` implementations backed by\n * [Vercel KV](https://vercel.com/docs/storage/vercel-kv) (Upstash Redis).\n *\n * # Why a separate subpath?\n *\n * `@vercel/kv` is a peer dependency — only consumers who actually use Vercel\n * KV install it. Importing from `@ar-agents/mercadopago/vercel-kv` is\n * lazy: the main `@ar-agents/mercadopago` bundle stays tiny for callers who\n * use the in-memory adapters or a different store.\n *\n * # Setup\n *\n * 1. Create a KV store at https://vercel.com/dashboard/stores\n * 2. Connect it to your project — Vercel auto-injects `KV_*` env vars\n * 3. `pnpm add @vercel/kv`\n * 4. Wire the adapters:\n *\n * ```ts\n * import { mercadoPagoTools, MercadoPagoClient } from \"@ar-agents/mercadopago\";\n * import {\n * VercelKVSubscriptionStateAdapter,\n * VercelKVOAuthTokenStore,\n * } from \"@ar-agents/mercadopago/vercel-kv\";\n *\n * const tools = mercadoPagoTools(client, {\n * state: new VercelKVSubscriptionStateAdapter(),\n * backUrl: \"https://mysite.com/done\",\n * // ... oauth, webhookSecret, etc.\n * });\n *\n * // For marketplace flows, also wire the OAuth token store:\n * const oauthStore = new VercelKVOAuthTokenStore();\n * await oauthStore.set(token.user_id, {\n * user_id: token.user_id,\n * access_token: token.access_token,\n * refresh_token: token.refresh_token!,\n * expires_at: Date.now() + (token.expires_in ?? 21600) * 1000,\n * });\n * ```\n *\n * # Edge Runtime\n *\n * `@vercel/kv` works in Vercel Edge Runtime, Node.js, and any environment\n * with `fetch` (it's a thin REST client over Upstash). All adapters here\n * are async and Edge-safe.\n *\n * # Key namespacing\n *\n * Each adapter uses its own prefix so multiple adapters can share the same\n * KV store without collisions:\n * - Subscriptions: `mp:sub:{id}`\n * - OAuth tokens: `mp:oauth:{userId}`\n * - Idempotency: `mp:idem:{key}`\n *\n * Pass a custom prefix via the constructor if you need to share the store\n * with other apps.\n */\n\nimport { kv as defaultKv } from \"@vercel/kv\";\nimport type { VercelKV } from \"@vercel/kv\";\nimport type { AuditEntry, AuditLogAdapter, AuditOperation } from \"./audit\";\nimport type {\n IdempotencyCache,\n OAuthTokenRecord,\n OAuthTokenStore,\n SubscriptionStateAdapter,\n SubscriptionStateRecord,\n} from \"./state\";\n\nconst DEFAULT_SUBSCRIPTION_PREFIX = \"mp:sub:\";\nconst DEFAULT_OAUTH_PREFIX = \"mp:oauth:\";\nconst DEFAULT_IDEMPOTENCY_PREFIX = \"mp:idem:\";\nconst DEFAULT_AUDIT_PREFIX = \"mp:audit:\";\n\ninterface VercelKVAdapterOptions {\n /**\n * Custom KV client. If omitted, uses the default `kv` export from\n * `@vercel/kv` (which reads `KV_REST_API_URL` + `KV_REST_API_TOKEN` from\n * env — auto-injected when you connect a KV store to your Vercel project).\n */\n kv?: VercelKV;\n /** Override the key prefix. */\n prefix?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SubscriptionStateAdapter\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVSubscriptionStateAdapter\n implements SubscriptionStateAdapter\n{\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_SUBSCRIPTION_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(id: string): string {\n return `${this.prefix}${id}`;\n }\n\n async set(\n id: string,\n state: Partial<SubscriptionStateRecord>,\n ): Promise<void> {\n const existing = (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? {};\n await this.kv.set(this.key(id), { ...existing, ...state });\n await this.kv.sadd(this.indexKey, id);\n }\n\n async get(id: string): Promise<SubscriptionStateRecord | null> {\n return (await this.kv.get<SubscriptionStateRecord>(this.key(id))) ?? null;\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n\n /** Forget a subscription record. NOT part of the adapter interface. */\n async delete(id: string): Promise<void> {\n await this.kv.del(this.key(id));\n await this.kv.srem(this.indexKey, id);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// OAuthTokenStore (per-seller marketplace token persistence)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVOAuthTokenStore implements OAuthTokenStore {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n private readonly indexKey: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_OAUTH_PREFIX;\n this.indexKey = `${this.prefix}__index`;\n }\n\n private key(userId: string): string {\n return `${this.prefix}${userId}`;\n }\n\n async set(userId: string, token: OAuthTokenRecord): Promise<void> {\n await this.kv.set(this.key(userId), token);\n await this.kv.sadd(this.indexKey, userId);\n }\n\n async get(userId: string): Promise<OAuthTokenRecord | null> {\n return (await this.kv.get<OAuthTokenRecord>(this.key(userId))) ?? null;\n }\n\n async delete(userId: string): Promise<void> {\n await this.kv.del(this.key(userId));\n await this.kv.srem(this.indexKey, userId);\n }\n\n async list(): Promise<string[]> {\n const ids = await this.kv.smembers(this.indexKey);\n return ids.map(String);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// IdempotencyCache (KV-backed dedup of agent retries)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class VercelKVIdempotencyCache implements IdempotencyCache {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_IDEMPOTENCY_PREFIX;\n }\n\n private key(k: string): string {\n return `${this.prefix}${k}`;\n }\n\n async get<T>(key: string): Promise<T | null> {\n return (await this.kv.get<T>(this.key(key))) ?? null;\n }\n\n async set<T>(key: string, value: T, ttlSeconds = 86_400): Promise<void> {\n // Vercel KV's `set` supports a TTL in seconds via the `ex` option.\n await this.kv.set(this.key(key), value, { ex: ttlSeconds });\n }\n\n async delete(key: string): Promise<void> {\n await this.kv.del(this.key(key));\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// AuditLogAdapter — production audit trail with daily-bucket indexing\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Vercel KV–backed audit log adapter. Stores each entry under\n * `mp:audit:entry:{id}` AND adds the id to a daily index sorted set\n * `mp:audit:day:{YYYY-MM-DD}` (score = timestamp ms). This gives O(log N)\n * time-range queries (\"all entries from May 1 to May 5\") without scanning\n * the entire log.\n *\n * # Storage layout\n *\n * - `mp:audit:entry:{id}` → the full entry JSON\n * - `mp:audit:day:{YYYY-MM-DD}` → ZSET of entry ids by timestamp (ms)\n * - `mp:audit:actor:{actor}` → ZSET of entry ids by timestamp (for \"all\n * entries by actor X\")\n * - `mp:audit:tenant:{tenantId}` → same, by tenant\n *\n * # Cost considerations\n *\n * Each `append()` does 1-3 KV writes (entry + 1-2 indexes). For high-traffic\n * deployments (>10/s sustained), batch via your own queue (e.g., Vercel\n * Queues with daily flush) and provide a custom adapter that batches.\n */\nexport class VercelKVAuditLog implements AuditLogAdapter {\n private readonly kv: VercelKV;\n private readonly prefix: string;\n\n constructor(options: VercelKVAdapterOptions = {}) {\n this.kv = options.kv ?? defaultKv;\n this.prefix = options.prefix ?? DEFAULT_AUDIT_PREFIX;\n }\n\n async append(entry: AuditEntry): Promise<void> {\n const ts = new Date(entry.timestamp).getTime();\n const day = entry.timestamp.slice(0, 10); // YYYY-MM-DD\n await Promise.all([\n this.kv.set(`${this.prefix}entry:${entry.id}`, entry),\n this.kv.zadd(`${this.prefix}day:${day}`, { score: ts, member: entry.id }),\n this.kv.zadd(`${this.prefix}actor:${entry.actor}`, { score: ts, member: entry.id }),\n ...(entry.tenantId\n ? [\n this.kv.zadd(`${this.prefix}tenant:${entry.tenantId}`, {\n score: ts,\n member: entry.id,\n }),\n ]\n : []),\n ]);\n }\n\n async query(filter: {\n actor?: string;\n operation?: AuditOperation;\n tenantId?: string;\n from?: string;\n to?: string;\n limit?: number;\n }): Promise<AuditEntry[]> {\n const limit = filter.limit ?? 100;\n let ids: string[];\n\n // Pick the most selective index available\n if (filter.actor) {\n ids = await this.zrangeByScore(\n `${this.prefix}actor:${filter.actor}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.tenantId) {\n ids = await this.zrangeByScore(\n `${this.prefix}tenant:${filter.tenantId}`,\n filter.from,\n filter.to,\n limit,\n );\n } else if (filter.from || filter.to) {\n // Walk daily buckets for the date range\n const fromDate = filter.from?.slice(0, 10) ?? \"0000-00-00\";\n const toDate = filter.to?.slice(0, 10) ?? \"9999-99-99\";\n ids = [];\n // Cap walk to ~1 year max to avoid runaway\n const fromTs = new Date(fromDate).getTime();\n const toTs = new Date(toDate).getTime();\n for (let d = fromTs; d <= toTs && ids.length < limit; d += 86_400_000) {\n const day = new Date(d).toISOString().slice(0, 10);\n const dayIds = await this.zrangeByScore(\n `${this.prefix}day:${day}`,\n filter.from,\n filter.to,\n limit - ids.length,\n );\n ids.push(...dayIds);\n }\n } else {\n // No filter — bail (full scan would be unbounded)\n return [];\n }\n\n // Load entries\n const entries: AuditEntry[] = [];\n for (const id of ids) {\n const entry = await this.kv.get<AuditEntry>(`${this.prefix}entry:${id}`);\n if (!entry) continue;\n if (filter.operation && entry.operation !== filter.operation) continue;\n entries.push(entry);\n }\n return entries;\n }\n\n private async zrangeByScore(\n key: string,\n from?: string,\n to?: string,\n limit?: number,\n ): Promise<string[]> {\n const min = from ? new Date(from).getTime() : 0;\n const max = to ? new Date(to).getTime() : Number.MAX_SAFE_INTEGER;\n const opts = {\n byScore: true as const,\n offset: 0,\n ...(limit !== undefined ? { count: limit } : { count: 100 }),\n };\n const ids = await this.kv.zrange(key, min, max, opts);\n return ids.map(String);\n }\n}\n"]}
|
package/dist/vercel-kv.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VercelKV } from '@vercel/kv';
|
|
2
|
-
import { I as IdempotencyCache,
|
|
2
|
+
import { c as AuditLogAdapter, b as AuditEntry, a as AuditOperation, I as IdempotencyCache, h as OAuthTokenStore, O as OAuthTokenRecord, S as SubscriptionStateAdapter, i as SubscriptionStateRecord } from './audit-B9Nhj3PH.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Vercel KV adapters — drop-in `SubscriptionStateAdapter`,
|
|
@@ -103,5 +103,41 @@ declare class VercelKVIdempotencyCache implements IdempotencyCache {
|
|
|
103
103
|
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
|
104
104
|
delete(key: string): Promise<void>;
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Vercel KV–backed audit log adapter. Stores each entry under
|
|
108
|
+
* `mp:audit:entry:{id}` AND adds the id to a daily index sorted set
|
|
109
|
+
* `mp:audit:day:{YYYY-MM-DD}` (score = timestamp ms). This gives O(log N)
|
|
110
|
+
* time-range queries ("all entries from May 1 to May 5") without scanning
|
|
111
|
+
* the entire log.
|
|
112
|
+
*
|
|
113
|
+
* # Storage layout
|
|
114
|
+
*
|
|
115
|
+
* - `mp:audit:entry:{id}` → the full entry JSON
|
|
116
|
+
* - `mp:audit:day:{YYYY-MM-DD}` → ZSET of entry ids by timestamp (ms)
|
|
117
|
+
* - `mp:audit:actor:{actor}` → ZSET of entry ids by timestamp (for "all
|
|
118
|
+
* entries by actor X")
|
|
119
|
+
* - `mp:audit:tenant:{tenantId}` → same, by tenant
|
|
120
|
+
*
|
|
121
|
+
* # Cost considerations
|
|
122
|
+
*
|
|
123
|
+
* Each `append()` does 1-3 KV writes (entry + 1-2 indexes). For high-traffic
|
|
124
|
+
* deployments (>10/s sustained), batch via your own queue (e.g., Vercel
|
|
125
|
+
* Queues with daily flush) and provide a custom adapter that batches.
|
|
126
|
+
*/
|
|
127
|
+
declare class VercelKVAuditLog implements AuditLogAdapter {
|
|
128
|
+
private readonly kv;
|
|
129
|
+
private readonly prefix;
|
|
130
|
+
constructor(options?: VercelKVAdapterOptions);
|
|
131
|
+
append(entry: AuditEntry): Promise<void>;
|
|
132
|
+
query(filter: {
|
|
133
|
+
actor?: string;
|
|
134
|
+
operation?: AuditOperation;
|
|
135
|
+
tenantId?: string;
|
|
136
|
+
from?: string;
|
|
137
|
+
to?: string;
|
|
138
|
+
limit?: number;
|
|
139
|
+
}): Promise<AuditEntry[]>;
|
|
140
|
+
private zrangeByScore;
|
|
141
|
+
}
|
|
106
142
|
|
|
107
|
-
export { VercelKVIdempotencyCache, VercelKVOAuthTokenStore, VercelKVSubscriptionStateAdapter };
|
|
143
|
+
export { VercelKVAuditLog, VercelKVIdempotencyCache, VercelKVOAuthTokenStore, VercelKVSubscriptionStateAdapter };
|