meterflow 0.4.4 → 0.5.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 +35 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/index.cjs +22 -1
- package/dist/index.js +22 -1
- package/dist/index.mjs +22 -1
- package/dist/resources/entitlements.d.ts +29 -0
- package/dist/resources/entitlements.d.ts.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Track what your customers use, enforce credit balances, and manage subscriptions
|
|
|
23
23
|
- [Usage events](#usage-events)
|
|
24
24
|
- [Subscriptions](#subscriptions)
|
|
25
25
|
- [Plans](#plans)
|
|
26
|
+
- [Entitlements — ask before you act](#entitlements--ask-before-you-act)
|
|
26
27
|
- [Idempotency — safe retries for writes](#idempotency--safe-retries-for-writes)
|
|
27
28
|
- [Automatic retries](#automatic-retries)
|
|
28
29
|
- [Error handling](#error-handling)
|
|
@@ -298,6 +299,38 @@ plan.meter_limits; // included units + overage rate per meter
|
|
|
298
299
|
|
|
299
300
|
Typical use: render your pricing page or signup flow from `plans.list()` so it can never drift from what billing actually enforces.
|
|
300
301
|
|
|
302
|
+
### Entitlements — ask before you act
|
|
303
|
+
|
|
304
|
+
The one call to make **before** doing work for a customer: "may they use this feature — and how much is left?" A **hard** limit says no here; recording usage afterwards never blocks, so an app that skips this call gets an honest ledger but no enforcement.
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
const gate = await client.entitlements.check("customer_123", "images.generated");
|
|
308
|
+
|
|
309
|
+
if (!gate.allowed) {
|
|
310
|
+
// gate.reason: "hard_limit_reached" | "no_subscription" | "not_in_plan" | "insufficient_credits"
|
|
311
|
+
return showUpgradePrompt(gate); // gate.used / gate.included / gate.period_end are there for the copy
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await generateImage();
|
|
315
|
+
await client.usage.record({ event_name: "images.generated", customer_external_id: "customer_123", value: 1, properties: {} });
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- `feature` is a meter's `event_name` (metered — `included`, `used`, `remaining` filled in) or a plan feature key like `"sso"` (boolean).
|
|
319
|
+
- `{ quantity: 5 }` asks "may they do **5** more?" — one call instead of one per item for batch work.
|
|
320
|
+
- A **soft** limit answers `allowed: true` with `reason: "soft_limit_exceeded"` — let it through, nudge the upgrade.
|
|
321
|
+
- A **metered** limit stays allowed while the customer's credits cover the overage; otherwise `insufficient_credits`.
|
|
322
|
+
- `allowed: false` is a normal resolved answer, never an exception. A feature key the project does not know at all is a `NotFoundError` — that is a typo, not a plan.
|
|
323
|
+
- The answer carries `Cache-Control: private, max-age=15`; on hot paths cache it per customer for a few seconds rather than calling on every request.
|
|
324
|
+
|
|
325
|
+
**All of them at once** — for a settings or pricing screen:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
const { entitlements, period_end } = await client.entitlements.get("customer_123");
|
|
329
|
+
// [{ feature: "sso", feature_type: "boolean", allowed: true }, { feature: "images.generated", feature_type: "metered", used: "120", included: 500, remaining: "380", ... }]
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Prefer to be told rather than to ask? Subscribe a webhook to `limit.reached` (see below).
|
|
333
|
+
|
|
301
334
|
## Idempotency — safe retries for writes
|
|
302
335
|
|
|
303
336
|
Connections drop. When your app can't tell whether a write arrived, the correct move is to send it again **with the same idempotency key** — MeterFlow recognises the key and acts only once:
|
|
@@ -367,7 +400,7 @@ try {
|
|
|
367
400
|
|
|
368
401
|
## Verifying webhooks
|
|
369
402
|
|
|
370
|
-
MeterFlow notifies your app of activity you'd otherwise poll for — credits granted or deducted, usage recorded, subscriptions created or updated. Every delivery is signed with **HMAC-SHA256** in the `X-MeterFlow-Signature` header, using the webhook's secret — verify before trusting:
|
|
403
|
+
MeterFlow notifies your app of activity you'd otherwise poll for — credits granted or deducted, usage recorded, subscriptions created or updated, a customer reaching a plan limit. Every delivery is signed with **HMAC-SHA256** in the `X-MeterFlow-Signature` header, using the webhook's secret — verify before trusting:
|
|
371
404
|
|
|
372
405
|
```typescript
|
|
373
406
|
import express from "express";
|
|
@@ -406,6 +439,7 @@ Every delivery is a flat JSON object (`Content-Type: application/json`) with two
|
|
|
406
439
|
| `credit.deducted` | credits are deducted | `customer_id`, `amount`, `balance_after`, `transaction_id` |
|
|
407
440
|
| `subscription.created` | a subscription is created | `customer_id`, `plan_id`, `subscription_id`, `status` |
|
|
408
441
|
| `subscription.updated` | a subscription is updated | `customer_id`, `subscription_id`, `status` |
|
|
442
|
+
| `limit.reached` | a customer first reaches a **hard** or **soft** limit's included units this billing period (once per meter per period; metered limits report through `credit.deducted` instead) | `customer_id`, `subscription_id`, `feature` (the meter's `event_name`), `meter_id`, `limit_type`, `included`, `used`, `period_start`, `period_end` |
|
|
409
443
|
|
|
410
444
|
For example, a `credit.granted` delivery:
|
|
411
445
|
|
package/dist/client.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { CreditsResource } from "./resources/credits";
|
|
|
2
2
|
import { UsageResource } from "./resources/usage";
|
|
3
3
|
import { SubscriptionsResource } from "./resources/subscriptions";
|
|
4
4
|
import { PlansResource } from "./resources/plans";
|
|
5
|
+
import { EntitlementsResource } from "./resources/entitlements";
|
|
5
6
|
export interface MeterFlowOptions {
|
|
6
7
|
apiKey: string;
|
|
7
8
|
baseUrl?: string;
|
|
@@ -21,11 +22,13 @@ export declare class MeterFlow {
|
|
|
21
22
|
private _usage?;
|
|
22
23
|
private _subscriptions?;
|
|
23
24
|
private _plans?;
|
|
25
|
+
private _entitlements?;
|
|
24
26
|
constructor(options: MeterFlowOptions);
|
|
25
27
|
get credits(): CreditsResource;
|
|
26
28
|
get usage(): UsageResource;
|
|
27
29
|
get subscriptions(): SubscriptionsResource;
|
|
28
30
|
get plans(): PlansResource;
|
|
31
|
+
get entitlements(): EntitlementsResource;
|
|
29
32
|
/** @internal — used by resource classes only; not part of the public API */
|
|
30
33
|
request<T>(method: string, path: string, opts?: RequestOptions): Promise<T>;
|
|
31
34
|
}
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,SAAS;IACpB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IAE7C,OAAO,CAAC,QAAQ,CAAC,CAAkB;IACnC,OAAO,CAAC,MAAM,CAAC,CAAgB;IAC/B,OAAO,CAAC,cAAc,CAAC,CAAwB;IAC/C,OAAO,CAAC,MAAM,CAAC,CAAgB;IAC/B,OAAO,CAAC,aAAa,CAAC,CAAuB;gBAEjC,OAAO,EAAE,gBAAgB;IAerC,IAAI,OAAO,IAAI,eAAe,CAE7B;IAED,IAAI,KAAK,IAAI,aAAa,CAEzB;IAED,IAAI,aAAa,IAAI,qBAAqB,CAEzC;IAED,IAAI,KAAK,IAAI,aAAa,CAEzB;IAED,IAAI,YAAY,IAAI,oBAAoB,CAEvC;IAED,4EAA4E;IAC5E,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;CAG5E"}
|
package/dist/index.cjs
CHANGED
|
@@ -35,7 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(index_exports);
|
|
36
36
|
|
|
37
37
|
// package.json
|
|
38
|
-
var version = "0.
|
|
38
|
+
var version = "0.5.0";
|
|
39
39
|
|
|
40
40
|
// src/errors.ts
|
|
41
41
|
var MeterFlowError = class extends Error {
|
|
@@ -330,6 +330,23 @@ var PlansResource = class {
|
|
|
330
330
|
}
|
|
331
331
|
};
|
|
332
332
|
|
|
333
|
+
// src/resources/entitlements.ts
|
|
334
|
+
var EntitlementsResource = class {
|
|
335
|
+
constructor(client) {
|
|
336
|
+
this.client = client;
|
|
337
|
+
}
|
|
338
|
+
/** Every feature on the customer's plan, with this period's usage. Empty `entitlements` without a subscription. */
|
|
339
|
+
get(customerId) {
|
|
340
|
+
return this.client.request("GET", `entitlements/${encodeURIComponent(customerId)}`);
|
|
341
|
+
}
|
|
342
|
+
/** May this customer use `feature` (`quantity` more units of it) right now? */
|
|
343
|
+
check(customerId, feature, opts) {
|
|
344
|
+
const query = { feature };
|
|
345
|
+
if (opts?.quantity !== void 0) query["quantity"] = opts.quantity;
|
|
346
|
+
return this.client.request("GET", `entitlements/${encodeURIComponent(customerId)}/check`, { query });
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
333
350
|
// src/client.ts
|
|
334
351
|
var MeterFlow = class {
|
|
335
352
|
options;
|
|
@@ -337,6 +354,7 @@ var MeterFlow = class {
|
|
|
337
354
|
_usage;
|
|
338
355
|
_subscriptions;
|
|
339
356
|
_plans;
|
|
357
|
+
_entitlements;
|
|
340
358
|
constructor(options) {
|
|
341
359
|
if (!options.apiKey.startsWith("mf_live_") && !options.apiKey.startsWith("mf_test_")) {
|
|
342
360
|
throw new Error("apiKey must start with mf_live_ or mf_test_");
|
|
@@ -363,6 +381,9 @@ var MeterFlow = class {
|
|
|
363
381
|
get plans() {
|
|
364
382
|
return this._plans ??= new PlansResource(this);
|
|
365
383
|
}
|
|
384
|
+
get entitlements() {
|
|
385
|
+
return this._entitlements ??= new EntitlementsResource(this);
|
|
386
|
+
}
|
|
366
387
|
/** @internal — used by resource classes only; not part of the public API */
|
|
367
388
|
request(method, path, opts) {
|
|
368
389
|
return httpRequest(this.options, method, path, opts);
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(index_exports);
|
|
36
36
|
|
|
37
37
|
// package.json
|
|
38
|
-
var version = "0.
|
|
38
|
+
var version = "0.5.0";
|
|
39
39
|
|
|
40
40
|
// src/errors.ts
|
|
41
41
|
var MeterFlowError = class extends Error {
|
|
@@ -330,6 +330,23 @@ var PlansResource = class {
|
|
|
330
330
|
}
|
|
331
331
|
};
|
|
332
332
|
|
|
333
|
+
// src/resources/entitlements.ts
|
|
334
|
+
var EntitlementsResource = class {
|
|
335
|
+
constructor(client) {
|
|
336
|
+
this.client = client;
|
|
337
|
+
}
|
|
338
|
+
/** Every feature on the customer's plan, with this period's usage. Empty `entitlements` without a subscription. */
|
|
339
|
+
get(customerId) {
|
|
340
|
+
return this.client.request("GET", `entitlements/${encodeURIComponent(customerId)}`);
|
|
341
|
+
}
|
|
342
|
+
/** May this customer use `feature` (`quantity` more units of it) right now? */
|
|
343
|
+
check(customerId, feature, opts) {
|
|
344
|
+
const query = { feature };
|
|
345
|
+
if (opts?.quantity !== void 0) query["quantity"] = opts.quantity;
|
|
346
|
+
return this.client.request("GET", `entitlements/${encodeURIComponent(customerId)}/check`, { query });
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
333
350
|
// src/client.ts
|
|
334
351
|
var MeterFlow = class {
|
|
335
352
|
options;
|
|
@@ -337,6 +354,7 @@ var MeterFlow = class {
|
|
|
337
354
|
_usage;
|
|
338
355
|
_subscriptions;
|
|
339
356
|
_plans;
|
|
357
|
+
_entitlements;
|
|
340
358
|
constructor(options) {
|
|
341
359
|
if (!options.apiKey.startsWith("mf_live_") && !options.apiKey.startsWith("mf_test_")) {
|
|
342
360
|
throw new Error("apiKey must start with mf_live_ or mf_test_");
|
|
@@ -363,6 +381,9 @@ var MeterFlow = class {
|
|
|
363
381
|
get plans() {
|
|
364
382
|
return this._plans ??= new PlansResource(this);
|
|
365
383
|
}
|
|
384
|
+
get entitlements() {
|
|
385
|
+
return this._entitlements ??= new EntitlementsResource(this);
|
|
386
|
+
}
|
|
366
387
|
/** @internal — used by resource classes only; not part of the public API */
|
|
367
388
|
request(method, path, opts) {
|
|
368
389
|
return httpRequest(this.options, method, path, opts);
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// package.json
|
|
2
|
-
var version = "0.
|
|
2
|
+
var version = "0.5.0";
|
|
3
3
|
|
|
4
4
|
// src/errors.ts
|
|
5
5
|
var MeterFlowError = class extends Error {
|
|
@@ -294,6 +294,23 @@ var PlansResource = class {
|
|
|
294
294
|
}
|
|
295
295
|
};
|
|
296
296
|
|
|
297
|
+
// src/resources/entitlements.ts
|
|
298
|
+
var EntitlementsResource = class {
|
|
299
|
+
constructor(client) {
|
|
300
|
+
this.client = client;
|
|
301
|
+
}
|
|
302
|
+
/** Every feature on the customer's plan, with this period's usage. Empty `entitlements` without a subscription. */
|
|
303
|
+
get(customerId) {
|
|
304
|
+
return this.client.request("GET", `entitlements/${encodeURIComponent(customerId)}`);
|
|
305
|
+
}
|
|
306
|
+
/** May this customer use `feature` (`quantity` more units of it) right now? */
|
|
307
|
+
check(customerId, feature, opts) {
|
|
308
|
+
const query = { feature };
|
|
309
|
+
if (opts?.quantity !== void 0) query["quantity"] = opts.quantity;
|
|
310
|
+
return this.client.request("GET", `entitlements/${encodeURIComponent(customerId)}/check`, { query });
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
297
314
|
// src/client.ts
|
|
298
315
|
var MeterFlow = class {
|
|
299
316
|
options;
|
|
@@ -301,6 +318,7 @@ var MeterFlow = class {
|
|
|
301
318
|
_usage;
|
|
302
319
|
_subscriptions;
|
|
303
320
|
_plans;
|
|
321
|
+
_entitlements;
|
|
304
322
|
constructor(options) {
|
|
305
323
|
if (!options.apiKey.startsWith("mf_live_") && !options.apiKey.startsWith("mf_test_")) {
|
|
306
324
|
throw new Error("apiKey must start with mf_live_ or mf_test_");
|
|
@@ -327,6 +345,9 @@ var MeterFlow = class {
|
|
|
327
345
|
get plans() {
|
|
328
346
|
return this._plans ??= new PlansResource(this);
|
|
329
347
|
}
|
|
348
|
+
get entitlements() {
|
|
349
|
+
return this._entitlements ??= new EntitlementsResource(this);
|
|
350
|
+
}
|
|
330
351
|
/** @internal — used by resource classes only; not part of the public API */
|
|
331
352
|
request(method, path, opts) {
|
|
332
353
|
return httpRequest(this.options, method, path, opts);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MeterFlow } from "../client";
|
|
2
|
+
import type { components } from "../types/openapi";
|
|
3
|
+
type EntitlementResponse = components["schemas"]["EntitlementResponse"];
|
|
4
|
+
type EntitlementsResponse = components["schemas"]["EntitlementsResponse"];
|
|
5
|
+
export interface CheckOptions {
|
|
6
|
+
/** How many more units the app is about to use — "may they do 5 more?". Defaults to 1 on the server. */
|
|
7
|
+
quantity?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Ask BEFORE doing the work. `check()` is where a hard limit says no; recording usage afterwards
|
|
11
|
+
* never blocks, so an app that skips this call gets no enforcement at all — only an honest ledger.
|
|
12
|
+
*
|
|
13
|
+
* `allowed: false` is a normal answer, not an error: the promise resolves and `reason` says why
|
|
14
|
+
* (`no_subscription`, `not_in_plan`, `hard_limit_reached`, `insufficient_credits`). A feature key
|
|
15
|
+
* the project does not know at all is a 404 → `NotFoundError`, because that is a typo, not a plan.
|
|
16
|
+
*
|
|
17
|
+
* Both calls return `Cache-Control: private, max-age=15` — cache per customer for a few seconds
|
|
18
|
+
* on hot paths rather than calling on every request.
|
|
19
|
+
*/
|
|
20
|
+
export declare class EntitlementsResource {
|
|
21
|
+
private readonly client;
|
|
22
|
+
constructor(client: MeterFlow);
|
|
23
|
+
/** Every feature on the customer's plan, with this period's usage. Empty `entitlements` without a subscription. */
|
|
24
|
+
get(customerId: string): Promise<EntitlementsResponse>;
|
|
25
|
+
/** May this customer use `feature` (`quantity` more units of it) right now? */
|
|
26
|
+
check(customerId: string, feature: string, opts?: CheckOptions): Promise<EntitlementResponse>;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=entitlements.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entitlements.d.ts","sourceRoot":"","sources":["../../../src/resources/entitlements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,KAAK,mBAAmB,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,qBAAqB,CAAC,CAAC;AACxE,KAAK,oBAAoB,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,sBAAsB,CAAC,CAAC;AAE1E,MAAM,WAAW,YAAY;IAC3B,wGAAwG;IACxG,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,SAAS;IAE9C,mHAAmH;IACnH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAItD,+EAA+E;IAC/E,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAK9F"}
|