@meterly/sdk 0.1.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 ADDED
@@ -0,0 +1,353 @@
1
+ # Meterly JavaScript SDK
2
+
3
+ Small TypeScript SDK for integrating Meterly prepaid usage control and credit workflows into Node.js and Next.js apps. `guard()` is the preferred path for protected operations that may fail after work starts. Credits are usage entitlement units, not money.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 18 or newer
8
+ - Meterly command API reachable, usually `http://localhost:5000`
9
+ - Meterly query API reachable, usually `http://localhost:5001`
10
+
11
+ The SDK uses native `fetch`. There are no runtime dependencies.
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ cd sdk/js
17
+ npm install
18
+ npm run build
19
+ ```
20
+
21
+ ```ts
22
+ import { MeterlyClient } from "@meterly/sdk";
23
+
24
+ const meterly = new MeterlyClient({
25
+ commandApiUrl: "http://localhost:5000",
26
+ queryApiUrl: "http://localhost:5001",
27
+ apiKey: "dev-meterly-key"
28
+ });
29
+
30
+ const grant = await meterly.grantCredits(
31
+ {
32
+ externalCustomerId: "customer_123",
33
+ creditAmount: 1000,
34
+ creditUnit: "image_credits",
35
+ reference: "order_123",
36
+ description: "1000 image credits purchase",
37
+ provider: "revenuecat",
38
+ providerEventId: "rc_evt_123"
39
+ },
40
+ {
41
+ idempotencyKey: "grant-order_123"
42
+ }
43
+ );
44
+
45
+ const balances = await meterly.getBalances(grant.grant.customerId);
46
+ console.log(balances.balances);
47
+ ```
48
+
49
+ ## Direct Credit Grants
50
+
51
+ Verify purchase state in your own backend, then grant credits directly when Meterly is the spend and balance source of truth:
52
+
53
+ ```ts
54
+ await meterly.grantCredits(
55
+ {
56
+ externalCustomerId: "user_123",
57
+ creditAmount: 1000,
58
+ creditUnit: "image_credits",
59
+ reference: "order_123",
60
+ description: "1000 image credits purchase",
61
+ provider: "revenuecat",
62
+ providerEventId: "rc_evt_123",
63
+ providerEventType: "INITIAL_PURCHASE"
64
+ },
65
+ { idempotencyKey: "grant-order_123" }
66
+ );
67
+ ```
68
+
69
+ Meterly creates the customer/account if missing, stores optional provider proof metadata, and applies credits through the ledger-backed AddCredits path. Tenant API keys are server-side only; do not use them in browser or mobile clients.
70
+
71
+ ## Guard Helper
72
+
73
+ `guard` wraps reservation, callback execution, commit, and release. The preferred form accepts a single options object:
74
+
75
+ ```ts
76
+ const result = await meterly.guard(
77
+ {
78
+ customerId,
79
+ amount: 250,
80
+ unit: "image_credits",
81
+ idempotencyKey: "guard-image-generation-001",
82
+ reference: "image-generation",
83
+ description: "Reserve credits for expensive work",
84
+ ttlSeconds: 300
85
+ },
86
+ async ({ reservationId }) => {
87
+ const output = await doExpensiveWork();
88
+ return {
89
+ reservationId,
90
+ output
91
+ };
92
+ }
93
+ );
94
+ ```
95
+
96
+ The positional form remains supported for compatibility:
97
+
98
+ ```ts
99
+ const result = await meterly.guard(
100
+ customerId,
101
+ 250,
102
+ async ({ reservationId }) => {
103
+ const output = await doExpensiveWork();
104
+ return {
105
+ reservationId,
106
+ output
107
+ };
108
+ },
109
+ {
110
+ reference: "image-generation",
111
+ description: "Reserve credits for expensive work",
112
+ ttlSeconds: 300
113
+ }
114
+ );
115
+ ```
116
+
117
+ Behavior:
118
+
119
+ - reserves credits first
120
+ - commits the reservation if the callback succeeds
121
+ - releases the reservation if the callback throws
122
+ - rethrows the original callback error after a successful release
123
+ - throws `MeterlyGuardError` if both the callback and the release fail
124
+
125
+ ## Idempotency
126
+
127
+ Every command mutation can send an `idempotencyKey` option. The SDK forwards it as the `Idempotency-Key` header:
128
+
129
+ ```ts
130
+ await meterly.grantCredits(
131
+ {
132
+ externalCustomerId: "customer_123",
133
+ creditAmount: 100,
134
+ reference: "support-adjustment-001"
135
+ },
136
+ { idempotencyKey: "grant-support-adjustment-001" }
137
+ );
138
+ ```
139
+
140
+ Use a stable key for retried operations so network retries and duplicate client submits do not apply credits twice.
141
+
142
+ ## Usage Units
143
+
144
+ Usage units let one tenant use separate entitlement units, such as `credits`, `image_credits`, `tokens`, or `gpu_seconds`. The default unit is `credits`.
145
+
146
+ ```ts
147
+ const units = await meterly.listUsageUnits();
148
+
149
+ await meterly.createUsageUnit({
150
+ code: "gpu_seconds",
151
+ displayName: "GPU seconds",
152
+ description: "Runtime authorization unit for GPU jobs"
153
+ });
154
+
155
+ await meterly.guard(
156
+ {
157
+ customerId,
158
+ amount: 30,
159
+ unit: "gpu_seconds",
160
+ reference: "gpu-job-42",
161
+ idempotencyKey: "reserve-gpu-job-42"
162
+ },
163
+ async () => runGpuJob()
164
+ );
165
+ ```
166
+
167
+ Package and provider-mapping flows inherit the package unit. There is no cross-unit conversion; reserve, commit, release, and usage reads operate on the unit you provide.
168
+
169
+ API keys also scope data access to a tenant. In local development, the default tenant comes from:
170
+
171
+ - API key: `dev-meterly-key`
172
+ - tenant: `dev-tenant`
173
+
174
+ Meterly can also issue persistent keys from `POST /api/v1/api-keys` or the admin console. Generated keys start with `mly_test_`, are stored hashed at rest, and the raw key is shown only once when created. Use the generated raw key as `apiKey` in `MeterlyClient`.
175
+
176
+ ## HMAC Signing
177
+
178
+ HMAC signing is optional and intended for production backend services that want replay protection beyond bearer-key authentication. Create a key with `signingMode: "hmac"`, store the raw key once on your server, and configure the SDK:
179
+
180
+ ```ts
181
+ const meterly = new MeterlyClient({
182
+ commandApiUrl: process.env.METERLY_COMMAND_API_URL!,
183
+ queryApiUrl: process.env.METERLY_QUERY_API_URL!,
184
+ apiKey: process.env.METERLY_API_KEY!,
185
+ signingMode: "hmac"
186
+ });
187
+ ```
188
+
189
+ For HMAC keys, every request still sends `Authorization: Bearer <api_key>`. The SDK also sends:
190
+
191
+ - `X-Meterly-Timestamp`
192
+ - `X-Meterly-Body-SHA256`
193
+ - `X-Meterly-Signature`
194
+
195
+ Canonical string:
196
+
197
+ ```text
198
+ METHOD
199
+ PATH_WITH_QUERY
200
+ BODY_SHA256
201
+ TIMESTAMP
202
+ ```
203
+
204
+ The HMAC secret is the raw API key, which Meterly shows only once and never stores in plaintext. Do not use tenant API keys or HMAC signing in browser or mobile clients.
205
+
206
+ ## Supported Methods
207
+
208
+ ```ts
209
+ await meterly.createCustomer(input, options);
210
+ await meterly.grantCredits(input, options);
211
+ await meterly.listCreditGrants(options);
212
+ await meterly.addCredits(customerId, amount, options);
213
+ await meterly.useCredits(customerId, amount, options);
214
+ await meterly.reserveCredits(customerId, amount, options);
215
+ await meterly.commitReservation(reservationId);
216
+ await meterly.releaseReservation(reservationId);
217
+ await meterly.getBalance(customerId);
218
+ await meterly.getBalances(customerId);
219
+ await meterly.getUsage(customerId, options);
220
+ await meterly.getLedgerEntries(customerId, options);
221
+ await meterly.getUsageSummary();
222
+ await meterly.listUsageUnits();
223
+ await meterly.createUsageUnit(input);
224
+ await meterly.guard({ customerId, amount, idempotencyKey }, callback);
225
+ await meterly.guard(customerId, amount, callback, options);
226
+ ```
227
+
228
+ `useCredits` is the direct/simple usage debit method. For AI/API work where execution can fail after authorization, prefer object-form `guard()` or the explicit `reserveCredits` -> `commitReservation` / `releaseReservation` flow.
229
+
230
+ Request options:
231
+
232
+ - `reference`
233
+ - `description`
234
+ - `metadata`
235
+ - `ttlSeconds`
236
+ - `idempotencyKey`
237
+ - `unit` or `creditUnit`
238
+
239
+ Current backend compatibility:
240
+
241
+ - `reference` and `description` are sent to Meterly
242
+ - `ttlSeconds` is translated into reservation `expiresAt`
243
+ - `unit` and `creditUnit` are sent as `creditUnit` on credit-affecting writes
244
+ - `idempotencyKey` is sent as the `Idempotency-Key` HTTP header on command mutations
245
+ - `metadata` is typed for future backend support but is not sent today
246
+
247
+ Retry behavior:
248
+
249
+ - the first request for a given `Idempotency-Key` executes normally and stores the response
250
+ - retries with the same key and the same request payload return the stored response
251
+ - retries with the same key and a different request payload return `409 Conflict`
252
+ - concurrent retries with the same key while the first request is still running return `409 Conflict`
253
+
254
+ ## Next.js API Route Example
255
+
256
+ ```ts
257
+ import { NextRequest, NextResponse } from "next/server";
258
+ import { MeterlyClient, MeterlyGuardError } from "@meterly/sdk";
259
+
260
+ const meterly = new MeterlyClient({
261
+ commandApiUrl: process.env.METERLY_COMMAND_API_URL ?? "http://localhost:5000",
262
+ queryApiUrl: process.env.METERLY_QUERY_API_URL ?? "http://localhost:5001",
263
+ apiKey: process.env.METERLY_API_KEY
264
+ });
265
+
266
+ export async function POST(request: NextRequest) {
267
+ const body = await request.json();
268
+
269
+ try {
270
+ const result = await meterly.guard(
271
+ {
272
+ customerId: body.customerId,
273
+ amount: body.credits,
274
+ idempotencyKey: body.idempotencyKey,
275
+ reference: body.reference,
276
+ description: "Protected Meterly operation",
277
+ ttlSeconds: 300
278
+ },
279
+ async () => {
280
+ return await runProtectedWork(body);
281
+ }
282
+ );
283
+
284
+ return NextResponse.json({ ok: true, result });
285
+ } catch (error) {
286
+ if (error instanceof MeterlyGuardError) {
287
+ return NextResponse.json(
288
+ {
289
+ ok: false,
290
+ message: error.message,
291
+ reservationId: error.reservationId
292
+ },
293
+ { status: 500 }
294
+ );
295
+ }
296
+
297
+ return NextResponse.json(
298
+ {
299
+ ok: false,
300
+ message: error instanceof Error ? error.message : "Unknown error"
301
+ },
302
+ { status: 500 }
303
+ );
304
+ }
305
+ }
306
+ ```
307
+
308
+ ## Local Demo Usage
309
+
310
+ From the repository root:
311
+
312
+ ```bash
313
+ docker compose up --build
314
+ ```
315
+
316
+ You can exercise the SDK directly against the same local stack:
317
+
318
+ ```ts
319
+ import { MeterlyClient } from "@meterly/sdk";
320
+
321
+ const meterly = new MeterlyClient({
322
+ commandApiUrl: "http://localhost:5000",
323
+ queryApiUrl: "http://localhost:5001",
324
+ apiKey: "dev-meterly-key"
325
+ });
326
+
327
+ const granted = await meterly.grantCredits(
328
+ {
329
+ externalCustomerId: `demo_${Date.now()}`,
330
+ creditAmount: 1000,
331
+ reference: "local-demo"
332
+ },
333
+ { idempotencyKey: "grant-local-demo" }
334
+ );
335
+
336
+ await meterly.useCredits(granted.grant.customerId, 120, {
337
+ reference: "demo-usage"
338
+ });
339
+
340
+ // For protected work, prefer object-form meterly.guard(...) so failed work releases the hold.
341
+
342
+ const usage = await meterly.getUsage(created.customerId);
343
+ console.log(usage.usage);
344
+ ```
345
+
346
+ ## Development
347
+
348
+ ```bash
349
+ npm install
350
+ npm run typecheck
351
+ npm test
352
+ npm run build
353
+ ```
@@ -0,0 +1,54 @@
1
+ import type { BaseResponse, BalancesResult, CheckoutSessionResult, CreateCheckoutSessionOptions, CreateCustomerInput, CreateCustomerResult, CreateUsageUnitInput, CreditGrantsListResult, CreditPackageGrantsListResult, CreditAccount, GrantCreditsInput, GrantCreditsResult, GrantCreditPackageInput, GrantCreditPackageResult, GuardCallback, GuardOptions, LedgerEntriesQueryOptions, ListCreditGrantsOptions, LedgerEntriesResult, ListCreditPackageGrantsOptions, ListStripePaymentsOptions, MeterlyClientConfig, MeterlyRequestOptions, PaymentProviderStatus, ReserveCreditsOptions, StripePaymentResult, StripePaymentsListResult, StripeReconcileResult, UsageQueryOptions, UsageResult, UsageUnitResult, UsageUnitsListResult, UsageSummary } from "./types";
2
+ interface ApiEnvelope {
3
+ message?: string;
4
+ }
5
+ interface ReservationResponse extends ApiEnvelope {
6
+ reservationId?: string;
7
+ }
8
+ export declare class MeterlyClient {
9
+ private readonly commandApiUrl;
10
+ private readonly queryApiUrl;
11
+ private readonly apiKey?;
12
+ private readonly signingMode;
13
+ private readonly fetchImpl;
14
+ private readonly extraHeaders?;
15
+ constructor(config: MeterlyClientConfig);
16
+ createCustomer(input: CreateCustomerInput, options?: MeterlyRequestOptions): Promise<CreateCustomerResult>;
17
+ addCredits(customerId: string, amount: number, options?: MeterlyRequestOptions): Promise<BaseResponse>;
18
+ useCredits(customerId: string, amount: number, options?: MeterlyRequestOptions): Promise<BaseResponse>;
19
+ reserveCredits(customerId: string, amount: number, options?: ReserveCreditsOptions): Promise<Required<Pick<ReservationResponse, "reservationId">> & BaseResponse>;
20
+ commitReservation(reservationId: string, options?: MeterlyRequestOptions): Promise<BaseResponse>;
21
+ releaseReservation(reservationId: string, options?: MeterlyRequestOptions): Promise<BaseResponse>;
22
+ /** @deprecated Legacy checkout compatibility only; not part of the current RevenueCat-first product surface. */
23
+ createCheckoutSession(customerId: string, packageId: string, options?: CreateCheckoutSessionOptions): Promise<CheckoutSessionResult>;
24
+ grantCreditPackage(packageId: string, input: GrantCreditPackageInput, options?: Pick<MeterlyRequestOptions, "idempotencyKey">): Promise<GrantCreditPackageResult>;
25
+ grantCredits(input: GrantCreditsInput, options?: Pick<MeterlyRequestOptions, "idempotencyKey">): Promise<GrantCreditsResult>;
26
+ listCreditGrants(options?: ListCreditGrantsOptions): Promise<CreditGrantsListResult>;
27
+ listCreditPackageGrants(options?: ListCreditPackageGrantsOptions): Promise<CreditPackageGrantsListResult>;
28
+ /** @deprecated Legacy Stripe status compatibility only; current provider setup is RevenueCat-first. */
29
+ getPaymentProviderStatus(): Promise<PaymentProviderStatus>;
30
+ listUsageUnits(status?: string): Promise<UsageUnitsListResult>;
31
+ createUsageUnit(input: CreateUsageUnitInput): Promise<UsageUnitResult>;
32
+ /** @deprecated Legacy Stripe compatibility only. */
33
+ listStripePayments(options?: ListStripePaymentsOptions): Promise<StripePaymentsListResult>;
34
+ /** @deprecated Legacy Stripe compatibility only. */
35
+ getStripePayment(id: string): Promise<StripePaymentResult>;
36
+ /** @deprecated Legacy Stripe compatibility only. */
37
+ reconcileStripePayment(id: string): Promise<StripeReconcileResult>;
38
+ /** @deprecated Legacy Stripe compatibility only. */
39
+ repairStripePayment(id: string): Promise<StripePaymentResult>;
40
+ getBalance(customerId: string): Promise<CreditAccount | null>;
41
+ getBalances(customerId: string): Promise<BalancesResult>;
42
+ getUsage(customerId: string, options?: UsageQueryOptions): Promise<UsageResult>;
43
+ getLedgerEntries(customerId: string, options?: LedgerEntriesQueryOptions): Promise<LedgerEntriesResult>;
44
+ getUsageSummary(): Promise<UsageSummary | null>;
45
+ guard<TResult>(customerId: string, amount: number, callback: GuardCallback<TResult>, options?: ReserveCreditsOptions): Promise<TResult>;
46
+ guard<TResult>(options: GuardOptions, callback: GuardCallback<TResult>): Promise<TResult>;
47
+ private normalizeGuardArgs;
48
+ private request;
49
+ private requestNullable;
50
+ private buildHeaders;
51
+ private buildMutationHeaders;
52
+ private parseResponse;
53
+ }
54
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,439 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MeterlyClient = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const errors_1 = require("./errors");
6
+ function normalizeBaseUrl(value, name) {
7
+ const trimmed = value.trim().replace(/\/+$/, "");
8
+ if (!trimmed) {
9
+ throw new Error(`${name} is required`);
10
+ }
11
+ return trimmed;
12
+ }
13
+ function assertIntegerAmount(amount) {
14
+ if (!Number.isInteger(amount) || amount <= 0) {
15
+ throw new TypeError("Meterly credit amounts must be positive integers.");
16
+ }
17
+ }
18
+ function assertTtlSeconds(ttlSeconds) {
19
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
20
+ throw new TypeError("ttlSeconds must be a positive integer.");
21
+ }
22
+ }
23
+ function buildQuery(baseUrl, params) {
24
+ const url = new URL(baseUrl);
25
+ for (const [key, value] of Object.entries(params)) {
26
+ if (value !== undefined && value !== "") {
27
+ url.searchParams.set(key, String(value));
28
+ }
29
+ }
30
+ return url.toString();
31
+ }
32
+ function toExpiresAt(ttlSeconds) {
33
+ if (ttlSeconds === undefined) {
34
+ return undefined;
35
+ }
36
+ assertTtlSeconds(ttlSeconds);
37
+ return new Date(Date.now() + ttlSeconds * 1000).toISOString();
38
+ }
39
+ function buildMutationPayload(amount, options) {
40
+ assertIntegerAmount(amount);
41
+ const creditUnit = options?.creditUnit ?? options?.unit;
42
+ return {
43
+ amount,
44
+ ...(creditUnit ? { creditUnit } : {}),
45
+ ...(options?.reference ? { reference: options.reference } : {}),
46
+ ...(options?.description ? { description: options.description } : {})
47
+ };
48
+ }
49
+ function isObject(value) {
50
+ return typeof value === "object" && value !== null;
51
+ }
52
+ function extractMessage(value, fallback) {
53
+ if (isObject(value) && typeof value.message === "string" && value.message.trim()) {
54
+ return value.message;
55
+ }
56
+ return fallback;
57
+ }
58
+ function createApiError(method, url, status, responseBody, statusText) {
59
+ const message = extractMessage(responseBody, statusText || `HTTP ${status}`);
60
+ const details = { status, method, url, responseBody };
61
+ if (status === 409 &&
62
+ typeof message === "string" &&
63
+ message.toLowerCase().includes("insufficient credits")) {
64
+ return new errors_1.MeterlyInsufficientCreditsError(message, details);
65
+ }
66
+ return new errors_1.MeterlyApiError(message, details);
67
+ }
68
+ function normalizeSigningMode(value) {
69
+ if (value === undefined || value === "bearer") {
70
+ return "bearer";
71
+ }
72
+ if (value === "hmac") {
73
+ return "hmac";
74
+ }
75
+ throw new Error("signingMode must be \"bearer\" or \"hmac\".");
76
+ }
77
+ function bodyToString(body) {
78
+ if (body === undefined || body === null) {
79
+ return "";
80
+ }
81
+ if (typeof body === "string") {
82
+ return body;
83
+ }
84
+ if (body instanceof URLSearchParams) {
85
+ return body.toString();
86
+ }
87
+ throw new Error("Meterly HMAC signing supports string request bodies.");
88
+ }
89
+ function sha256Hex(value) {
90
+ return (0, node_crypto_1.createHash)("sha256").update(value).digest("hex");
91
+ }
92
+ function pathWithQuery(url) {
93
+ const parsed = new URL(url);
94
+ return `${parsed.pathname || "/"}${parsed.search}`;
95
+ }
96
+ class MeterlyClient {
97
+ commandApiUrl;
98
+ queryApiUrl;
99
+ apiKey;
100
+ signingMode;
101
+ fetchImpl;
102
+ extraHeaders;
103
+ constructor(config) {
104
+ this.commandApiUrl = normalizeBaseUrl(config.commandApiUrl, "commandApiUrl");
105
+ this.queryApiUrl = normalizeBaseUrl(config.queryApiUrl, "queryApiUrl");
106
+ this.apiKey = config.apiKey;
107
+ this.signingMode = normalizeSigningMode(config.signingMode);
108
+ this.fetchImpl = config.fetch ?? globalThis.fetch;
109
+ this.extraHeaders = config.headers;
110
+ if (!this.fetchImpl) {
111
+ throw new Error("Fetch is required. Use Node.js 18+ or provide a fetch implementation.");
112
+ }
113
+ }
114
+ async createCustomer(input, options) {
115
+ return this.request("POST", `${this.commandApiUrl}/api/v1/customers`, {
116
+ body: JSON.stringify({
117
+ customerExternalId: input.customerExternalId,
118
+ ...(input.creditUnit ? { creditUnit: input.creditUnit } : {})
119
+ }),
120
+ headers: this.buildMutationHeaders(options)
121
+ });
122
+ }
123
+ async addCredits(customerId, amount, options) {
124
+ return this.request("POST", `${this.commandApiUrl}/api/v1/customers/${encodeURIComponent(customerId)}/credits/add`, {
125
+ body: JSON.stringify(buildMutationPayload(amount, options)),
126
+ headers: this.buildMutationHeaders(options)
127
+ });
128
+ }
129
+ async useCredits(customerId, amount, options) {
130
+ return this.request("POST", `${this.commandApiUrl}/api/v1/customers/${encodeURIComponent(customerId)}/credits/use`, {
131
+ body: JSON.stringify(buildMutationPayload(amount, options)),
132
+ headers: this.buildMutationHeaders(options)
133
+ });
134
+ }
135
+ async reserveCredits(customerId, amount, options) {
136
+ const response = await this.request("POST", `${this.commandApiUrl}/api/v1/customers/${encodeURIComponent(customerId)}/credits/reserve`, {
137
+ body: JSON.stringify({
138
+ ...buildMutationPayload(amount, options),
139
+ ...(options?.reservationId ? { reservationId: options.reservationId } : {}),
140
+ ...(options?.ttlSeconds !== undefined
141
+ ? { expiresAt: toExpiresAt(options.ttlSeconds) }
142
+ : {})
143
+ }),
144
+ headers: this.buildMutationHeaders(options)
145
+ });
146
+ if (!response.reservationId) {
147
+ throw new errors_1.MeterlyApiError("Meterly reserveCredits response did not include reservationId.", {
148
+ status: 502,
149
+ method: "POST",
150
+ url: `${this.commandApiUrl}/api/v1/customers/${encodeURIComponent(customerId)}/credits/reserve`,
151
+ responseBody: response
152
+ });
153
+ }
154
+ return {
155
+ message: extractMessage(response, "Credits reserved."),
156
+ reservationId: response.reservationId
157
+ };
158
+ }
159
+ async commitReservation(reservationId, options) {
160
+ return this.request("POST", `${this.commandApiUrl}/api/v1/reservations/${encodeURIComponent(reservationId)}/commit`, { headers: this.buildMutationHeaders(options) });
161
+ }
162
+ async releaseReservation(reservationId, options) {
163
+ return this.request("POST", `${this.commandApiUrl}/api/v1/reservations/${encodeURIComponent(reservationId)}/release`, { headers: this.buildMutationHeaders(options) });
164
+ }
165
+ /** @deprecated Legacy checkout compatibility only; not part of the current RevenueCat-first product surface. */
166
+ async createCheckoutSession(customerId, packageId, options) {
167
+ const response = await this.request("POST", `${this.commandApiUrl}/api/v1/customers/${encodeURIComponent(customerId)}/checkout/sessions`, {
168
+ body: JSON.stringify({
169
+ packageId,
170
+ ...(options?.reference ? { reference: options.reference } : {})
171
+ }),
172
+ headers: this.buildMutationHeaders(options)
173
+ });
174
+ if (!response.checkoutUrl || !response.sessionId) {
175
+ throw new errors_1.MeterlyApiError("Meterly checkout session response missing checkoutUrl/sessionId.", {
176
+ status: 502,
177
+ method: "POST",
178
+ url: `${this.commandApiUrl}/api/v1/customers/${encodeURIComponent(customerId)}/checkout/sessions`,
179
+ responseBody: response
180
+ });
181
+ }
182
+ return {
183
+ message: extractMessage(response, "Checkout session created."),
184
+ checkoutUrl: response.checkoutUrl,
185
+ sessionId: response.sessionId
186
+ };
187
+ }
188
+ async grantCreditPackage(packageId, input, options) {
189
+ return this.request("POST", `${this.commandApiUrl}/api/v1/credit-packages/${encodeURIComponent(packageId)}/grants`, {
190
+ body: JSON.stringify(input),
191
+ headers: this.buildMutationHeaders(options)
192
+ });
193
+ }
194
+ async grantCredits(input, options) {
195
+ assertIntegerAmount(input.creditAmount);
196
+ return this.request("POST", `${this.commandApiUrl}/api/v1/credit-grants`, {
197
+ body: JSON.stringify(input),
198
+ headers: this.buildMutationHeaders(options)
199
+ });
200
+ }
201
+ async listCreditGrants(options = {}) {
202
+ return this.request("GET", buildQuery(`${this.commandApiUrl}/api/v1/credit-grants`, {
203
+ customerId: options.customerId,
204
+ externalCustomerId: options.externalCustomerId,
205
+ provider: options.provider,
206
+ providerPaymentId: options.providerPaymentId,
207
+ providerEventId: options.providerEventId
208
+ }));
209
+ }
210
+ async listCreditPackageGrants(options = {}) {
211
+ return this.request("GET", buildQuery(`${this.commandApiUrl}/api/v1/credit-package-grants`, {
212
+ customerId: options.customerId,
213
+ externalCustomerId: options.externalCustomerId,
214
+ packageId: options.packageId,
215
+ provider: options.provider,
216
+ providerPaymentId: options.providerPaymentId,
217
+ providerEventId: options.providerEventId
218
+ }));
219
+ }
220
+ /** @deprecated Legacy Stripe status compatibility only; current provider setup is RevenueCat-first. */
221
+ async getPaymentProviderStatus() {
222
+ return this.request("GET", `${this.commandApiUrl}/api/v1/payment-provider/status`);
223
+ }
224
+ async listUsageUnits(status) {
225
+ return this.request("GET", buildQuery(`${this.commandApiUrl}/api/v1/usage-units`, { status }));
226
+ }
227
+ async createUsageUnit(input) {
228
+ return this.request("POST", `${this.commandApiUrl}/api/v1/usage-units`, {
229
+ body: JSON.stringify(input),
230
+ headers: this.buildMutationHeaders()
231
+ });
232
+ }
233
+ /** @deprecated Legacy Stripe compatibility only. */
234
+ async listStripePayments(options = {}) {
235
+ return this.request("GET", buildQuery(`${this.commandApiUrl}/api/v1/payments/stripe`, {
236
+ status: options.status,
237
+ page: options.page,
238
+ pageSize: options.pageSize
239
+ }));
240
+ }
241
+ /** @deprecated Legacy Stripe compatibility only. */
242
+ async getStripePayment(id) {
243
+ return this.request("GET", `${this.commandApiUrl}/api/v1/payments/stripe/${encodeURIComponent(id)}`);
244
+ }
245
+ /** @deprecated Legacy Stripe compatibility only. */
246
+ async reconcileStripePayment(id) {
247
+ return this.request("POST", `${this.commandApiUrl}/api/v1/payments/stripe/${encodeURIComponent(id)}/reconcile`);
248
+ }
249
+ /** @deprecated Legacy Stripe compatibility only. */
250
+ async repairStripePayment(id) {
251
+ return this.request("POST", `${this.commandApiUrl}/api/v1/payments/stripe/${encodeURIComponent(id)}/repair`);
252
+ }
253
+ async getBalance(customerId) {
254
+ const response = await this.requestNullable("GET", `${this.queryApiUrl}/api/v1/customers/${encodeURIComponent(customerId)}/balance`);
255
+ return response?.account ?? null;
256
+ }
257
+ async getBalances(customerId) {
258
+ const response = await this.requestNullable("GET", `${this.queryApiUrl}/api/v1/customers/${encodeURIComponent(customerId)}/balances`);
259
+ return {
260
+ message: response?.message ?? "No balances found.",
261
+ customerId,
262
+ balances: response?.balances ?? []
263
+ };
264
+ }
265
+ async getUsage(customerId, options = {}) {
266
+ const response = await this.requestNullable("GET", buildQuery(`${this.queryApiUrl}/api/v1/customers/${encodeURIComponent(customerId)}/usage`, {
267
+ page: options.page,
268
+ pageSize: options.pageSize,
269
+ sortOrder: options.sortOrder,
270
+ type: options.type,
271
+ unit: options.unit,
272
+ search: options.search,
273
+ startAt: options.startAt,
274
+ endAt: options.endAt
275
+ }));
276
+ return {
277
+ message: response?.message ?? "No usage events found.",
278
+ usage: response?.usage ?? [],
279
+ pagination: response?.pagination
280
+ };
281
+ }
282
+ async getLedgerEntries(customerId, options = {}) {
283
+ const response = await this.requestNullable("GET", buildQuery(`${this.queryApiUrl}/api/v1/customers/${encodeURIComponent(customerId)}/ledger-entries`, {
284
+ page: options.page,
285
+ pageSize: options.pageSize,
286
+ sortOrder: options.sortOrder
287
+ }));
288
+ return {
289
+ message: response?.message ?? "No ledger entries found.",
290
+ ledgerEntries: response?.ledgerEntries ?? [],
291
+ pagination: response?.pagination
292
+ };
293
+ }
294
+ async getUsageSummary() {
295
+ const response = await this.requestNullable("GET", `${this.queryApiUrl}/api/v1/usage/summary`);
296
+ return response?.summary ?? null;
297
+ }
298
+ async guard(customerIdOrOptions, amountOrCallback, callbackOrOptions, maybeOptions) {
299
+ const { customerId, amount, callback, options } = this.normalizeGuardArgs(customerIdOrOptions, amountOrCallback, callbackOrOptions, maybeOptions);
300
+ const reservation = await this.reserveCredits(customerId, amount, options);
301
+ try {
302
+ const result = await callback({ reservationId: reservation.reservationId });
303
+ await this.commitReservation(reservation.reservationId);
304
+ return result;
305
+ }
306
+ catch (callbackError) {
307
+ try {
308
+ await this.releaseReservation(reservation.reservationId);
309
+ }
310
+ catch (releaseError) {
311
+ throw new errors_1.MeterlyGuardError({
312
+ customerId,
313
+ reservationId: reservation.reservationId,
314
+ callbackError,
315
+ releaseError
316
+ });
317
+ }
318
+ throw callbackError;
319
+ }
320
+ }
321
+ normalizeGuardArgs(customerIdOrOptions, amountOrCallback, callbackOrOptions, maybeOptions) {
322
+ if (typeof customerIdOrOptions === "string") {
323
+ if (typeof amountOrCallback !== "number" || typeof callbackOrOptions !== "function") {
324
+ throw new TypeError("guard(customerId, amount, callback, options) requires a callback.");
325
+ }
326
+ return {
327
+ customerId: customerIdOrOptions,
328
+ amount: amountOrCallback,
329
+ callback: callbackOrOptions,
330
+ options: maybeOptions
331
+ };
332
+ }
333
+ if (typeof amountOrCallback !== "function") {
334
+ throw new TypeError("guard(options, callback) requires a callback.");
335
+ }
336
+ const { customerId, amount, ...options } = customerIdOrOptions;
337
+ return {
338
+ customerId,
339
+ amount,
340
+ callback: amountOrCallback,
341
+ options
342
+ };
343
+ }
344
+ async request(method, url, init = {}) {
345
+ const response = await this.fetchImpl(url, {
346
+ ...init,
347
+ method,
348
+ headers: this.buildHeaders(method, url, init.headers, init.body),
349
+ cache: "no-store"
350
+ });
351
+ const parsed = await this.parseResponse(method, url, response, false);
352
+ if (parsed === null) {
353
+ throw new errors_1.MeterlyApiError("Meterly API returned an empty response body.", {
354
+ status: response.status,
355
+ method,
356
+ url
357
+ });
358
+ }
359
+ return parsed;
360
+ }
361
+ async requestNullable(method, url, init = {}) {
362
+ const response = await this.fetchImpl(url, {
363
+ ...init,
364
+ method,
365
+ headers: this.buildHeaders(method, url, init.headers, init.body),
366
+ cache: "no-store"
367
+ });
368
+ return this.parseResponse(method, url, response, true);
369
+ }
370
+ buildHeaders(method, url, headers, body) {
371
+ const built = new Headers(headers);
372
+ if (!built.has("Content-Type")) {
373
+ built.set("Content-Type", "application/json");
374
+ }
375
+ if (this.apiKey && !built.has("Authorization")) {
376
+ built.set("Authorization", `Bearer ${this.apiKey}`);
377
+ }
378
+ if (this.extraHeaders) {
379
+ for (const [key, value] of Object.entries(this.extraHeaders)) {
380
+ if (!built.has(key)) {
381
+ built.set(key, value);
382
+ }
383
+ }
384
+ }
385
+ if (this.signingMode === "hmac") {
386
+ if (!this.apiKey) {
387
+ throw new Error("Meterly HMAC signing requires apiKey.");
388
+ }
389
+ const timestamp = Math.floor(Date.now() / 1000).toString();
390
+ const bodyText = bodyToString(body);
391
+ const bodyHash = sha256Hex(bodyText);
392
+ const canonical = [
393
+ method.toUpperCase(),
394
+ pathWithQuery(url),
395
+ bodyHash,
396
+ timestamp
397
+ ].join("\n");
398
+ built.set("X-Meterly-Timestamp", timestamp);
399
+ built.set("X-Meterly-Body-SHA256", bodyHash);
400
+ built.set("X-Meterly-Signature", (0, node_crypto_1.createHmac)("sha256", this.apiKey).update(canonical).digest("hex"));
401
+ }
402
+ return built;
403
+ }
404
+ buildMutationHeaders(options) {
405
+ if (!options?.idempotencyKey) {
406
+ return undefined;
407
+ }
408
+ return {
409
+ "Idempotency-Key": options.idempotencyKey
410
+ };
411
+ }
412
+ async parseResponse(method, url, response, allowNoContent) {
413
+ if (response.status === 204) {
414
+ return allowNoContent ? null : {};
415
+ }
416
+ const text = await response.text();
417
+ let responseBody = undefined;
418
+ if (text) {
419
+ try {
420
+ responseBody = JSON.parse(text);
421
+ }
422
+ catch {
423
+ if (response.ok) {
424
+ throw new errors_1.MeterlyApiError("Received invalid JSON from Meterly API.", {
425
+ status: response.status,
426
+ method,
427
+ url,
428
+ responseBody: text
429
+ });
430
+ }
431
+ }
432
+ }
433
+ if (!response.ok) {
434
+ throw createApiError(method, url, response.status, responseBody, response.statusText);
435
+ }
436
+ return (responseBody ?? null);
437
+ }
438
+ }
439
+ exports.MeterlyClient = MeterlyClient;
@@ -0,0 +1,29 @@
1
+ export interface MeterlyApiErrorDetails {
2
+ status: number;
3
+ method: string;
4
+ url: string;
5
+ responseBody?: unknown;
6
+ }
7
+ export declare class MeterlyApiError extends Error {
8
+ readonly status: number;
9
+ readonly method: string;
10
+ readonly url: string;
11
+ readonly responseBody?: unknown;
12
+ constructor(message: string, details: MeterlyApiErrorDetails);
13
+ }
14
+ export declare class MeterlyInsufficientCreditsError extends MeterlyApiError {
15
+ constructor(message: string, details: MeterlyApiErrorDetails);
16
+ }
17
+ export interface MeterlyGuardErrorOptions {
18
+ customerId: string;
19
+ reservationId: string;
20
+ callbackError: unknown;
21
+ releaseError: unknown;
22
+ }
23
+ export declare class MeterlyGuardError extends Error {
24
+ readonly customerId: string;
25
+ readonly reservationId: string;
26
+ readonly callbackError: unknown;
27
+ readonly releaseError: unknown;
28
+ constructor(options: MeterlyGuardErrorOptions);
29
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MeterlyGuardError = exports.MeterlyInsufficientCreditsError = exports.MeterlyApiError = void 0;
4
+ class MeterlyApiError extends Error {
5
+ status;
6
+ method;
7
+ url;
8
+ responseBody;
9
+ constructor(message, details) {
10
+ super(message);
11
+ this.name = "MeterlyApiError";
12
+ this.status = details.status;
13
+ this.method = details.method;
14
+ this.url = details.url;
15
+ this.responseBody = details.responseBody;
16
+ }
17
+ }
18
+ exports.MeterlyApiError = MeterlyApiError;
19
+ class MeterlyInsufficientCreditsError extends MeterlyApiError {
20
+ constructor(message, details) {
21
+ super(message, details);
22
+ this.name = "MeterlyInsufficientCreditsError";
23
+ }
24
+ }
25
+ exports.MeterlyInsufficientCreditsError = MeterlyInsufficientCreditsError;
26
+ class MeterlyGuardError extends Error {
27
+ customerId;
28
+ reservationId;
29
+ callbackError;
30
+ releaseError;
31
+ constructor(options) {
32
+ super(`Meterly guard failed for customer ${options.customerId}: callback threw and reservation release also failed.`);
33
+ this.name = "MeterlyGuardError";
34
+ this.customerId = options.customerId;
35
+ this.reservationId = options.reservationId;
36
+ this.callbackError = options.callbackError;
37
+ this.releaseError = options.releaseError;
38
+ }
39
+ }
40
+ exports.MeterlyGuardError = MeterlyGuardError;
@@ -0,0 +1,3 @@
1
+ export { MeterlyClient } from "./client";
2
+ export { MeterlyApiError, MeterlyGuardError, MeterlyInsufficientCreditsError } from "./errors";
3
+ export type { BaseResponse, CheckoutSessionResult, CreateCheckoutSessionOptions, CreateCustomerInput, CreateCustomerResult, CreditAccount, CreditGrant, CreditGrantsListResult, CreditPackageGrant, CreditPackageGrantsListResult, CreditUsageEvent, GrantCreditsInput, GrantCreditsResult, GrantCreditPackageInput, GrantCreditPackageResult, GuardContext, GuardOptions, LedgerEntriesQueryOptions, LedgerEntriesResult, ListCreditGrantsOptions, ListCreditPackageGrantsOptions, MeterlyClientConfig, MeterlyRequestOptions, PaymentProviderStatus, ReserveCreditsOptions, ListStripePaymentsOptions, ReconcileOutcome, StripePayment, StripePaymentResult, StripePaymentStatus, StripePaymentsListResult, StripeReconcileResult, UsageQueryOptions, UsageResult, UsageSummary } from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MeterlyInsufficientCreditsError = exports.MeterlyGuardError = exports.MeterlyApiError = exports.MeterlyClient = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "MeterlyClient", { enumerable: true, get: function () { return client_1.MeterlyClient; } });
6
+ var errors_1 = require("./errors");
7
+ Object.defineProperty(exports, "MeterlyApiError", { enumerable: true, get: function () { return errors_1.MeterlyApiError; } });
8
+ Object.defineProperty(exports, "MeterlyGuardError", { enumerable: true, get: function () { return errors_1.MeterlyGuardError; } });
9
+ Object.defineProperty(exports, "MeterlyInsufficientCreditsError", { enumerable: true, get: function () { return errors_1.MeterlyInsufficientCreditsError; } });
@@ -0,0 +1,335 @@
1
+ export interface MeterlyClientConfig {
2
+ commandApiUrl: string;
3
+ queryApiUrl: string;
4
+ apiKey?: string;
5
+ signingMode?: "bearer" | "hmac";
6
+ fetch?: typeof fetch;
7
+ headers?: Record<string, string>;
8
+ }
9
+ export interface BaseResponse {
10
+ message: string;
11
+ }
12
+ export interface PaginationMeta {
13
+ page: number;
14
+ pageSize: number;
15
+ returnedItems: number;
16
+ hasMore: boolean;
17
+ sortBy?: string;
18
+ sortOrder?: "asc" | "desc";
19
+ }
20
+ export interface Customer {
21
+ id: string;
22
+ customerExternalId: string;
23
+ creditUnit: string;
24
+ createdAt: string;
25
+ }
26
+ export interface CreditAccount {
27
+ id: string;
28
+ customerId: string;
29
+ creditUnit: string;
30
+ balance: number;
31
+ reserved: number;
32
+ available: number;
33
+ createdAt: string;
34
+ updatedAt: string;
35
+ }
36
+ export interface CustomerRecord {
37
+ customer?: Customer;
38
+ account?: CreditAccount | null;
39
+ }
40
+ export type CreditAction = "ACCOUNT_CREATED" | "CREDITS_ADDED" | "CREDITS_USED" | "CREDITS_USE_REJECTED" | "CREDITS_RESERVED" | "CREDITS_COMMITTED" | "CREDITS_RELEASED" | "RESERVATION_EXPIRED" | string;
41
+ export interface CreditUsageEvent {
42
+ id: string;
43
+ customerId: string;
44
+ reservationId?: string;
45
+ eventId: string;
46
+ type: CreditAction;
47
+ amount: number;
48
+ creditUnit?: string;
49
+ reference?: string;
50
+ description?: string;
51
+ occurredAt: string;
52
+ createdAt: string;
53
+ }
54
+ export interface UsageUnit {
55
+ unitId: string;
56
+ tenantId: string;
57
+ code: string;
58
+ displayName: string;
59
+ description?: string;
60
+ precision: number;
61
+ status: "active" | "archived" | string;
62
+ createdAt: string;
63
+ updatedAt: string;
64
+ }
65
+ export interface LedgerEntry {
66
+ id: string;
67
+ customerId: string;
68
+ creditAccountId: string;
69
+ reservationId?: string;
70
+ eventId: string;
71
+ eventType: string;
72
+ eventVersion: number;
73
+ entryType: "DEBIT" | "CREDIT" | "RESERVE" | "RELEASE";
74
+ action: CreditAction;
75
+ amount: number;
76
+ balanceAfter: number;
77
+ availableAfter: number;
78
+ creditUnit: string;
79
+ reference?: string;
80
+ description?: string;
81
+ occurredAt: string;
82
+ createdAt: string;
83
+ }
84
+ export interface UsageSummary {
85
+ id: string;
86
+ customerCount: number;
87
+ /** Legacy cross-unit compatibility aggregate; prefer per-unit balance reads. */
88
+ totalBalance: number;
89
+ /** Legacy cross-unit compatibility aggregate; prefer per-unit balance reads. */
90
+ totalReserved: number;
91
+ /** Legacy cross-unit compatibility aggregate; prefer per-unit balance reads. */
92
+ totalAvailable: number;
93
+ /** Legacy cross-unit compatibility aggregate; prefer usageEventsByType or per-unit grant views. */
94
+ totalCreditsAdded: number;
95
+ /** Legacy cross-unit compatibility aggregate; prefer usageEventsByType or per-unit usage views. */
96
+ totalCreditsUsed: number;
97
+ totalReservations: number;
98
+ openReservations: number;
99
+ rejectedUsageEvents: number;
100
+ usageEventsByType?: Record<string, number>;
101
+ }
102
+ export interface CreateCustomerInput {
103
+ customerExternalId: string;
104
+ creditUnit?: string;
105
+ }
106
+ export interface CreateCustomerResult extends BaseResponse {
107
+ customerId: string;
108
+ }
109
+ export interface MeterlyRequestOptions {
110
+ reference?: string;
111
+ description?: string;
112
+ creditUnit?: string;
113
+ unit?: string;
114
+ metadata?: Record<string, unknown>;
115
+ idempotencyKey?: string;
116
+ }
117
+ export interface ReserveCreditsOptions extends MeterlyRequestOptions {
118
+ reservationId?: string;
119
+ ttlSeconds?: number;
120
+ }
121
+ export interface UsageQueryOptions {
122
+ page?: number;
123
+ pageSize?: number;
124
+ sortOrder?: "asc" | "desc";
125
+ type?: string;
126
+ unit?: string;
127
+ search?: string;
128
+ startAt?: string;
129
+ endAt?: string;
130
+ }
131
+ export interface LedgerEntriesQueryOptions {
132
+ page?: number;
133
+ pageSize?: number;
134
+ sortOrder?: "asc" | "desc";
135
+ }
136
+ export interface UsageResult {
137
+ message: string;
138
+ usage: CreditUsageEvent[];
139
+ pagination?: PaginationMeta;
140
+ }
141
+ export interface BalancesResult extends BaseResponse {
142
+ customerId: string;
143
+ balances: CreditAccount[];
144
+ }
145
+ export interface UsageUnitsListResult extends BaseResponse {
146
+ usageUnits: UsageUnit[];
147
+ }
148
+ export interface CreateUsageUnitInput {
149
+ code: string;
150
+ displayName?: string;
151
+ description?: string;
152
+ }
153
+ export interface UsageUnitResult extends BaseResponse {
154
+ usageUnit: UsageUnit;
155
+ }
156
+ export interface LedgerEntriesResult {
157
+ message: string;
158
+ ledgerEntries: LedgerEntry[];
159
+ pagination?: PaginationMeta;
160
+ }
161
+ export interface GuardContext {
162
+ reservationId: string;
163
+ }
164
+ export type GuardCallback<TResult> = (context: GuardContext) => Promise<TResult> | TResult;
165
+ export interface GuardOptions extends ReserveCreditsOptions {
166
+ customerId: string;
167
+ amount: number;
168
+ }
169
+ /** @deprecated Legacy checkout compatibility only; not part of the current RevenueCat-first product surface. */
170
+ export interface CreateCheckoutSessionOptions extends MeterlyRequestOptions {
171
+ }
172
+ /** @deprecated Legacy checkout compatibility only; not part of the current RevenueCat-first product surface. */
173
+ export interface CheckoutSessionResult extends BaseResponse {
174
+ checkoutUrl: string;
175
+ sessionId: string;
176
+ }
177
+ export interface GrantCreditPackageInput {
178
+ externalCustomerId: string;
179
+ reference: string;
180
+ description?: string;
181
+ provider?: string;
182
+ providerAccountId?: string;
183
+ providerPaymentId?: string;
184
+ providerEventId?: string;
185
+ providerEventType?: string;
186
+ verificationSource?: string;
187
+ }
188
+ export interface GrantCreditsInput {
189
+ externalCustomerId: string;
190
+ creditAmount: number;
191
+ creditUnit?: string;
192
+ reference: string;
193
+ description?: string;
194
+ provider?: string;
195
+ providerAccountId?: string;
196
+ providerPaymentId?: string;
197
+ providerEventId?: string;
198
+ providerEventType?: string;
199
+ verificationSource?: string;
200
+ }
201
+ export interface CreditGrant {
202
+ grantId: string;
203
+ tenantId: string;
204
+ customerId: string;
205
+ externalCustomerId: string;
206
+ creditAmount: number;
207
+ creditUnit: string;
208
+ reference: string;
209
+ description?: string;
210
+ provider?: string;
211
+ providerAccountId?: string;
212
+ providerPaymentId?: string;
213
+ providerEventId?: string;
214
+ providerEventType?: string;
215
+ verificationSource: string;
216
+ status: "pending" | "granted" | string;
217
+ createdAt: string;
218
+ }
219
+ export interface GrantCreditsResult extends BaseResponse {
220
+ grant: CreditGrant;
221
+ }
222
+ export interface ListCreditGrantsOptions {
223
+ customerId?: string;
224
+ externalCustomerId?: string;
225
+ provider?: string;
226
+ providerPaymentId?: string;
227
+ providerEventId?: string;
228
+ }
229
+ export interface CreditGrantsListResult extends BaseResponse {
230
+ grants: CreditGrant[];
231
+ }
232
+ export interface CreditPackageGrant {
233
+ grantId: string;
234
+ tenantId: string;
235
+ packageId: string;
236
+ customerId: string;
237
+ externalCustomerId: string;
238
+ creditAmount: number;
239
+ creditUnit: string;
240
+ priceAmount?: number;
241
+ priceCurrency?: string;
242
+ reference: string;
243
+ description?: string;
244
+ provider?: string;
245
+ providerAccountId?: string;
246
+ providerPaymentId?: string;
247
+ providerEventId?: string;
248
+ providerEventType?: string;
249
+ verificationSource: string;
250
+ status: "pending" | "granted" | string;
251
+ createdAt: string;
252
+ }
253
+ export interface GrantCreditPackageResult extends BaseResponse {
254
+ grant: CreditPackageGrant;
255
+ }
256
+ export interface ListCreditPackageGrantsOptions {
257
+ customerId?: string;
258
+ externalCustomerId?: string;
259
+ packageId?: string;
260
+ provider?: string;
261
+ providerPaymentId?: string;
262
+ providerEventId?: string;
263
+ }
264
+ export interface CreditPackageGrantsListResult extends BaseResponse {
265
+ grants: CreditPackageGrant[];
266
+ }
267
+ export interface HostedConnectStatus {
268
+ supported: boolean;
269
+ status: "coming_soon" | string;
270
+ message: string;
271
+ }
272
+ /** @deprecated Legacy Stripe status compatibility only; current provider setup is RevenueCat-first. */
273
+ export interface PaymentProviderStatus {
274
+ tenantId: string;
275
+ provider: "stripe" | string;
276
+ mode: "self_hosted" | string;
277
+ enabled: boolean;
278
+ configured: boolean;
279
+ checkoutAvailable: boolean;
280
+ webhookEndpoint: string;
281
+ successUrlConfigured: boolean;
282
+ cancelUrlConfigured: boolean;
283
+ webhookSecretConfigured: boolean;
284
+ secretKeyConfigured: boolean;
285
+ hostedConnect: HostedConnectStatus;
286
+ }
287
+ /** @deprecated Legacy Stripe compatibility only. */
288
+ export type StripePaymentStatus = "received" | "processed" | "duplicate" | "failed" | "reconciled" | string;
289
+ export type ReconcileOutcome = "reconciled" | "mismatch" | "pending" | "failed" | string;
290
+ /** @deprecated Legacy Stripe compatibility only. */
291
+ export interface StripePayment {
292
+ stripeEventId: string;
293
+ stripeSessionId?: string;
294
+ tenantId: string;
295
+ projectId?: string;
296
+ customerId: string;
297
+ packageId: string;
298
+ credits: number;
299
+ amount?: number;
300
+ currency?: string;
301
+ reference?: string;
302
+ status: StripePaymentStatus;
303
+ creditEventId?: string;
304
+ errorMessage?: string;
305
+ createdAt: string;
306
+ processedAt?: string;
307
+ reconciledAt?: string;
308
+ }
309
+ /** @deprecated Legacy Stripe compatibility only. */
310
+ export interface ListStripePaymentsOptions {
311
+ status?: StripePaymentStatus;
312
+ page?: number;
313
+ pageSize?: number;
314
+ }
315
+ /** @deprecated Legacy Stripe compatibility only. */
316
+ export interface StripePaymentsListResult extends BaseResponse {
317
+ payments: StripePayment[];
318
+ pagination: {
319
+ page: number;
320
+ pageSize: number;
321
+ hasMore: boolean;
322
+ };
323
+ }
324
+ /** @deprecated Legacy Stripe compatibility only. */
325
+ export interface StripePaymentResult extends BaseResponse {
326
+ payment: StripePayment;
327
+ }
328
+ /** @deprecated Legacy Stripe compatibility only. */
329
+ export interface StripeReconcileResult extends BaseResponse {
330
+ outcome: ReconcileOutcome;
331
+ status: StripePaymentStatus;
332
+ errorMessage?: string;
333
+ detail?: string;
334
+ payment: StripePayment;
335
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@meterly/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Developer-facing JavaScript SDK for Meterly credit workflows",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "typecheck": "tsc --noEmit -p tsconfig.json",
20
+ "test": "tsx test/guard.test.ts"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^24.9.1",
24
+ "tsx": "^4.20.6",
25
+ "typescript": "^5.9.3"
26
+ }
27
+ }