mbase-sdk 0.0.5 → 0.0.7

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/dist/index.d.ts CHANGED
@@ -432,6 +432,70 @@ type PlanChangeParams = {
432
432
  type PlanChangeNowParams = Omit<PlanChangeParams, "effective">;
433
433
  /** Neither `effective` nor `reconciliation` is open: the call names both. */
434
434
  type PlanMoveParams = Pick<PlanChangeParams, "plan">;
435
+ /** `awaiting`: the plan meters this, but its only versions land later. */
436
+ type EntitlementState = "granted" | "awaiting";
437
+ /** `used` counts toward `amount`; `total` adds what grants paid for. */
438
+ type MeterEntitlement = {
439
+ meter_id: string;
440
+ state: EntitlementState;
441
+ version: PlanAllowance | null;
442
+ amount: number | null;
443
+ no_cap: boolean;
444
+ used: number;
445
+ total: number;
446
+ remaining: number | null;
447
+ reserved: number;
448
+ available: number | null;
449
+ incoming: {
450
+ version: PlanAllowance;
451
+ lands_at: string;
452
+ } | null;
453
+ };
454
+ /** The current period only, and only the meters the plan meters. */
455
+ type CustomerEntitlement = {
456
+ customer_id: string;
457
+ plan_id: string;
458
+ cycle: PlanCycle;
459
+ since: string;
460
+ period: {
461
+ start: string;
462
+ end: string;
463
+ };
464
+ meters: MeterEntitlement[];
465
+ };
466
+ type UsagePeriod = "current" | "last";
467
+ /** 1–90 UTC days, ending today. */
468
+ type UsageDaysParams = {
469
+ days: number;
470
+ period?: never;
471
+ };
472
+ type UsagePeriodParams = {
473
+ period: UsagePeriod;
474
+ days?: never;
475
+ };
476
+ type CustomerUsageParams = UsageDaysParams | UsagePeriodParams;
477
+ type DayUsage = {
478
+ /** A UTC day, `YYYY-MM-DD`. */
479
+ date: string;
480
+ quantity: number;
481
+ };
482
+ /** Every day of the range that has begun, zeros included. */
483
+ type MeterUsage = {
484
+ meter_id: string;
485
+ total: number;
486
+ days: DayUsage[];
487
+ };
488
+ /** `[from, to)`. Only the meters with usage in the range. */
489
+ type CustomerUsage = {
490
+ customer_id: string;
491
+ from: string;
492
+ to: string;
493
+ meters: MeterUsage[];
494
+ };
495
+ type MeterUsageHistory = MeterUsage & {
496
+ from: string;
497
+ to: string;
498
+ };
435
499
  /** `plan` is not accepted on a grant: the engine mints those itself when a
436
500
  * plan is assigned. */
437
501
  type AllowanceSource = "purchased" | "bonus" | "manual";
@@ -527,6 +591,45 @@ type ReleaseResult = {
527
591
  reservation_id: string;
528
592
  quantity: number;
529
593
  };
594
+ /** What a threshold crossing reports, as `threshold.crossed` and `entitlement.reached` send it. */
595
+ type ThresholdCrossing = {
596
+ /** The tenant's own customer id and the meter key. */
597
+ customer_id: string;
598
+ meter_id: string;
599
+ /** The plan in force when it crossed; `null` if they held none. */
600
+ plan: {
601
+ id: string;
602
+ key: string;
603
+ name: string;
604
+ } | null;
605
+ /** A whole percent, 1–1000; past 100 is an overage alert. */
606
+ threshold: number;
607
+ /** The plan's figures at the crossing; neither includes grants. */
608
+ used: number;
609
+ entitlement: number;
610
+ period: {
611
+ start: string;
612
+ end: string;
613
+ };
614
+ usage_event_id: string | null;
615
+ };
616
+ /** Sent for every mark a customer crosses. */
617
+ type ThresholdCrossedEvent = {
618
+ /** The same on every attempt and every endpoint: dedupe on it. */
619
+ id: string;
620
+ type: "threshold.crossed";
621
+ timestamp: string;
622
+ data: ThresholdCrossing;
623
+ };
624
+ /** Sent for the 100% crossing too, under its own id. */
625
+ type EntitlementReachedEvent = {
626
+ id: string;
627
+ type: "entitlement.reached";
628
+ timestamp: string;
629
+ data: ThresholdCrossing;
630
+ };
631
+ /** New types can be added; ignore any you do not handle. */
632
+ type WebhookEvent = ThresholdCrossedEvent | EntitlementReachedEvent;
530
633
 
531
634
  /**
532
635
  * Capacity handed to one customer on top of their plan. A grant with
@@ -665,6 +768,14 @@ declare class Customers$1 {
665
768
  * an empty collection rather than a 404.
666
769
  */
667
770
  retrieveByExternalId(externalId: string, options?: RequestOptions): Promise<CustomerWithPlan | null>;
771
+ /** `null` for a customer holding no plan, as `plan.retrieve` answers. */
772
+ entitlement(id: string, options?: RequestOptions): Promise<CustomerEntitlement | null>;
773
+ /**
774
+ * Usage per UTC day. A period the customer never had, holding no plan now
775
+ * or none before this period, is `null`.
776
+ */
777
+ usage(id: string, params: UsageDaysParams, options?: RequestOptions): Promise<CustomerUsage>;
778
+ usage(id: string, params: UsagePeriodParams, options?: RequestOptions): Promise<CustomerUsage | null>;
668
779
  update(id: string, params: CustomerUpdateParams, options?: RequestOptions): Promise<Customer>;
669
780
  /** Soft delete: usage and assignments keep referencing the customer. */
670
781
  delete(id: string, options?: RequestOptions): Promise<Customer>;
@@ -677,6 +788,8 @@ declare class Meters$1 {
677
788
  list(params?: MeterListParams, options?: RequestOptions): Promise<List<Meter>>;
678
789
  retrieve(id: string, options?: RequestOptions): Promise<Meter>;
679
790
  update(id: string, params: MeterUpdateParams, options?: RequestOptions): Promise<Meter>;
791
+ /** Every customer's usage of the meter per UTC day. */
792
+ usage(id: string, params: UsageDaysParams, options?: RequestOptions): Promise<MeterUsageHistory>;
680
793
  /** Soft delete: plans and usage keep referencing the meter. Idempotent. */
681
794
  archive(id: string, options?: RequestOptions): Promise<Meter>;
682
795
  }
@@ -800,6 +913,29 @@ declare function errorFromResponse(args: {
800
913
  retryAfter?: number | undefined;
801
914
  body?: unknown;
802
915
  }): APIError;
916
+ /** A webhook that is not one Meterbase signed recently: answer it with a 400. */
917
+ declare class WebhookVerificationError extends MeterbaseError {
918
+ }
919
+
920
+ /** A `Headers` object, or Node's lowercase header record. */
921
+ type WebhookHeaders = {
922
+ get(name: string): string | null;
923
+ } | Record<string, string | string[] | undefined>;
924
+ type VerifyWebhookParams = {
925
+ /** The body exactly as received. Re-serialized JSON will not verify. */
926
+ payload: string | Uint8Array;
927
+ headers: WebhookHeaders;
928
+ /** The endpoint's `whsec_…` secret; pass both while rotating. */
929
+ secret: string | readonly string[];
930
+ /** How far `webhook-timestamp` may be from now. Five minutes by default. */
931
+ toleranceSeconds?: number;
932
+ };
933
+ /**
934
+ * Checks a delivery's signature and timestamp the Standard Webhooks way, then
935
+ * returns the event. Throws `WebhookVerificationError` when it is not one
936
+ * Meterbase signed recently.
937
+ */
938
+ declare function verifyWebhook(params: VerifyWebhookParams): Promise<WebhookEvent>;
803
939
 
804
940
  /**
805
941
  * A granted hold, with the two calls that close it. `commit` is `track` under
@@ -909,4 +1045,4 @@ type Meters = InstanceType<typeof Meters$1>;
909
1045
  type PlanAllowances = InstanceType<typeof PlanAllowances$1>;
910
1046
  type Plans = InstanceType<typeof Plans$1>;
911
1047
 
912
- export { APIError, type Allowance, type AllowanceGrantParams, type AllowanceListParams, type AllowanceSource, type ApplyMode, type AssignPlanParams, type Assignment, AuthenticationError, type CheckParams, type CheckRateLimit, type CheckReason, type CheckResult, ConflictError, ConnectionError, type Customer, type CustomerAllowances, type CustomerCreateParams, type CustomerListParams, type CustomerPlan, type CustomerUpdateParams, type CustomerWithPlan, type Customers, CycleChangeRequiresResetError, type EmbeddedPlan, type Hold, InvalidIdempotencyKeyError, InvalidRequestError, type List, type Meter, type MeterCreateParams, type MeterListParams, type MeterUpdateParams, Meterbase, MeterbaseError, type MeterbaseOptions, type Meters, NotFoundError, PermissionDeniedError, type Plan, type PlanAllowance, type PlanAllowanceKind, type PlanAllowanceListParams, type PlanAllowanceSetParams, type PlanAllowances, type PlanChangeNowParams, type PlanChangeParams, type PlanCreateParams, type PlanCycle, type PlanListParams, type PlanMoveParams, type PlanUpdateParams, type Plans, RateLimitError, type Reconciliation, type RecordedReconciliation, type ReleaseParams, type ReleaseResult, type RequestOptions, ReservationMismatchError, type ReserveParams, type ReserveRefused, type ReserveResponse, type ReserveResult, ServerError, TimeoutError, TooLateError, type TrackParams, type TrackResult, type UsageEvent, type Validity, type WhoAmI, errorFromResponse };
1048
+ export { APIError, type Allowance, type AllowanceGrantParams, type AllowanceListParams, type AllowanceSource, type ApplyMode, type AssignPlanParams, type Assignment, AuthenticationError, type CheckParams, type CheckRateLimit, type CheckReason, type CheckResult, ConflictError, ConnectionError, type Customer, type CustomerAllowances, type CustomerCreateParams, type CustomerEntitlement, type CustomerListParams, type CustomerPlan, type CustomerUpdateParams, type CustomerUsage, type CustomerUsageParams, type CustomerWithPlan, type Customers, CycleChangeRequiresResetError, type DayUsage, type EmbeddedPlan, type EntitlementReachedEvent, type EntitlementState, type Hold, InvalidIdempotencyKeyError, InvalidRequestError, type List, type Meter, type MeterCreateParams, type MeterEntitlement, type MeterListParams, type MeterUpdateParams, type MeterUsage, type MeterUsageHistory, Meterbase, MeterbaseError, type MeterbaseOptions, type Meters, NotFoundError, PermissionDeniedError, type Plan, type PlanAllowance, type PlanAllowanceKind, type PlanAllowanceListParams, type PlanAllowanceSetParams, type PlanAllowances, type PlanChangeNowParams, type PlanChangeParams, type PlanCreateParams, type PlanCycle, type PlanListParams, type PlanMoveParams, type PlanUpdateParams, type Plans, RateLimitError, type Reconciliation, type RecordedReconciliation, type ReleaseParams, type ReleaseResult, type RequestOptions, ReservationMismatchError, type ReserveParams, type ReserveRefused, type ReserveResponse, type ReserveResult, ServerError, type ThresholdCrossedEvent, type ThresholdCrossing, TimeoutError, TooLateError, type TrackParams, type TrackResult, type UsageDaysParams, type UsageEvent, type UsagePeriod, type UsagePeriodParams, type Validity, type VerifyWebhookParams, type WebhookEvent, type WebhookHeaders, WebhookVerificationError, type WhoAmI, errorFromResponse, verifyWebhook };
package/dist/index.js CHANGED
@@ -95,6 +95,8 @@ function serverTime(body) {
95
95
  const parsed = new Date(at);
96
96
  return Number.isNaN(parsed.getTime()) ? void 0 : parsed;
97
97
  }
98
+ var WebhookVerificationError = class extends MeterbaseError {
99
+ };
98
100
 
99
101
  // src/client.ts
100
102
  var DEFAULT_BASE_URL = "https://api.meterbase.tech";
@@ -135,7 +137,7 @@ var Client = class {
135
137
  this.#fetch = options.fetch ?? globalThis.fetch;
136
138
  if (typeof this.#fetch !== "function") {
137
139
  throw new MeterbaseError(
138
- "No fetch implementation: pass one as `fetch`, or run on Node 18+."
140
+ "No fetch implementation: pass one as `fetch`, or run on Node 20+."
139
141
  );
140
142
  }
141
143
  }
@@ -506,6 +508,36 @@ var Customers = class {
506
508
  });
507
509
  return data[0] ?? null;
508
510
  }
511
+ /** `null` for a customer holding no plan, as `plan.retrieve` answers. */
512
+ async entitlement(id, options) {
513
+ try {
514
+ return await this.#client.request({
515
+ ...options,
516
+ method: "GET",
517
+ path: `/v1/customers/${encodeURIComponent(id)}/entitlement`
518
+ });
519
+ } catch (error) {
520
+ if (error instanceof NotFoundError && error.code === "no_plan_assigned") {
521
+ return null;
522
+ }
523
+ throw error;
524
+ }
525
+ }
526
+ async usage(id, params, options) {
527
+ try {
528
+ return await this.#client.request({
529
+ ...options,
530
+ method: "GET",
531
+ path: `/v1/customers/${encodeURIComponent(id)}/usage`,
532
+ query: { days: params.days, period: params.period }
533
+ });
534
+ } catch (error) {
535
+ if (error instanceof NotFoundError && (error.code === "no_plan_assigned" || error.code === "no_previous_period")) {
536
+ return null;
537
+ }
538
+ throw error;
539
+ }
540
+ }
509
541
  update(id, params, options) {
510
542
  return this.#client.request({
511
543
  ...options,
@@ -561,6 +593,15 @@ var Meters = class {
561
593
  body: params
562
594
  });
563
595
  }
596
+ /** Every customer's usage of the meter per UTC day. */
597
+ usage(id, params, options) {
598
+ return this.#client.request({
599
+ ...options,
600
+ method: "GET",
601
+ path: `/v1/meters/${encodeURIComponent(id)}/usage`,
602
+ query: { days: params.days }
603
+ });
604
+ }
564
605
  /** Soft delete: plans and usage keep referencing the meter. Idempotent. */
565
606
  archive(id, options) {
566
607
  return this.#client.request({
@@ -647,6 +688,84 @@ var Plans = class {
647
688
  }
648
689
  };
649
690
 
691
+ // src/webhooks.ts
692
+ var SECRET_PREFIX = "whsec_";
693
+ var DEFAULT_TOLERANCE_SECONDS = 5 * 60;
694
+ async function verifyWebhook(params) {
695
+ const id = header(params.headers, "webhook-id");
696
+ const timestamp = header(params.headers, "webhook-timestamp");
697
+ const signatures = header(params.headers, "webhook-signature");
698
+ if (!id || !timestamp || !signatures) {
699
+ throw new WebhookVerificationError(
700
+ "Missing a webhook-id, webhook-timestamp or webhook-signature header."
701
+ );
702
+ }
703
+ const sentAt = Number(timestamp);
704
+ const tolerance = params.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
705
+ if (!Number.isInteger(sentAt) || Math.abs(Date.now() / 1e3 - sentAt) > tolerance) {
706
+ throw new WebhookVerificationError(
707
+ "The webhook-timestamp is too far from now; this may be a replay."
708
+ );
709
+ }
710
+ const body = typeof params.payload === "string" ? new TextEncoder().encode(params.payload) : params.payload;
711
+ const signed = concat(new TextEncoder().encode(`${id}.${timestamp}.`), body);
712
+ const candidates = signatures.split(" ").filter((entry) => entry.startsWith("v1,")).map((entry) => fromBase64(entry.slice(3)));
713
+ const subtle = webCrypto();
714
+ const secrets = typeof params.secret === "string" ? [params.secret] : params.secret;
715
+ for (const secret of secrets) {
716
+ const raw = secret.startsWith(SECRET_PREFIX) ? fromBase64(secret.slice(SECRET_PREFIX.length)) : void 0;
717
+ if (!raw) {
718
+ throw new WebhookVerificationError(
719
+ "The secret should be whsec_ and base64: copy it from the endpoint's settings."
720
+ );
721
+ }
722
+ const key = await subtle.importKey(
723
+ "raw",
724
+ raw,
725
+ { name: "HMAC", hash: "SHA-256" },
726
+ false,
727
+ ["verify"]
728
+ );
729
+ for (const candidate of candidates) {
730
+ if (candidate && await subtle.verify("HMAC", key, candidate, signed)) {
731
+ return JSON.parse(new TextDecoder().decode(body));
732
+ }
733
+ }
734
+ }
735
+ throw new WebhookVerificationError(
736
+ "No signature matches: the body changed, or this is the wrong secret."
737
+ );
738
+ }
739
+ function header(headers, name) {
740
+ if (typeof headers.get === "function") {
741
+ return headers.get(name) ?? void 0;
742
+ }
743
+ const value = headers[name];
744
+ return Array.isArray(value) ? value[0] : value;
745
+ }
746
+ function webCrypto() {
747
+ const subtle = globalThis.crypto?.subtle;
748
+ if (!subtle) {
749
+ throw new MeterbaseError(
750
+ "No Web Crypto to verify a webhook with: run on Node 20+, Deno, Bun or a Worker."
751
+ );
752
+ }
753
+ return subtle;
754
+ }
755
+ function fromBase64(text) {
756
+ try {
757
+ return Uint8Array.from(atob(text), (char) => char.charCodeAt(0));
758
+ } catch {
759
+ return void 0;
760
+ }
761
+ }
762
+ function concat(head, tail) {
763
+ const out = new Uint8Array(head.length + tail.length);
764
+ out.set(head);
765
+ out.set(tail, head.length);
766
+ return out;
767
+ }
768
+
650
769
  // src/index.ts
651
770
  var Meterbase = class {
652
771
  customers;
@@ -867,6 +986,6 @@ function idempotencyKey(atMs) {
867
986
  ].join("-");
868
987
  }
869
988
 
870
- export { APIError, AuthenticationError, ConflictError, ConnectionError, CycleChangeRequiresResetError, InvalidIdempotencyKeyError, InvalidRequestError, Meterbase, MeterbaseError, NotFoundError, PermissionDeniedError, RateLimitError, ReservationMismatchError, ServerError, TimeoutError, TooLateError, errorFromResponse };
989
+ export { APIError, AuthenticationError, ConflictError, ConnectionError, CycleChangeRequiresResetError, InvalidIdempotencyKeyError, InvalidRequestError, Meterbase, MeterbaseError, NotFoundError, PermissionDeniedError, RateLimitError, ReservationMismatchError, ServerError, TimeoutError, TooLateError, WebhookVerificationError, errorFromResponse, verifyWebhook };
871
990
  //# sourceMappingURL=index.js.map
872
991
  //# sourceMappingURL=index.js.map