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 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
  }
@@ -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;AAElD,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;gBAEnB,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,4EAA4E;IAC5E,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;CAG5E"}
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.4.4";
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.4.4";
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.4.4";
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meterflow",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Official Node.js SDK for MeterFlow — usage-based billing, credit management, and metering.",
5
5
  "license": "MIT",
6
6
  "author": "MeterFlow",