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/README.md +150 -7
- package/dist/index.cjs +122 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -1
- package/dist/index.d.ts +137 -1
- package/dist/index.js +121 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|