@clocknext/sdk 0.19.1 → 0.20.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 +74 -2
- package/dist/index.cjs +119 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -4
- package/dist/index.d.ts +151 -4
- package/dist/index.js +116 -4
- package/dist/index.js.map +1 -1
- package/examples/webhook-receiver.ts +70 -0
- package/package.json +1 -1
- package/src/client.ts +5 -0
- package/src/errors.ts +10 -0
- package/src/index.ts +20 -0
- package/src/resources/customers.ts +29 -0
- package/src/resources/signals.ts +2 -2
- package/src/types.ts +10 -0
- package/src/webhooks.ts +243 -0
package/dist/index.d.cts
CHANGED
|
@@ -748,10 +748,19 @@ interface WalletLedgerPage {
|
|
|
748
748
|
/** Reserved Wallet balance, USD; `null` when the workspace hasn't enabled
|
|
749
749
|
* it. */
|
|
750
750
|
reservedBalanceUsd: number | null;
|
|
751
|
+
/** The customer's own low-balance alert level, USD; `null` when none is set.
|
|
752
|
+
* At or below it the `wallet.balance.drop` webhook fires for this customer.
|
|
753
|
+
* Set with `customers.setWalletAlert()`. */
|
|
754
|
+
lowBalanceAlertUsd: number | null;
|
|
751
755
|
transactions: WalletTransaction[];
|
|
752
756
|
/** Pass back as `cursor` for the next page; `null` on the last page. */
|
|
753
757
|
nextCursor: string | null;
|
|
754
758
|
}
|
|
759
|
+
/** Result of `customers.setWalletAlert()`. */
|
|
760
|
+
interface WalletAlert {
|
|
761
|
+
/** The saved low-balance alert level, USD; `null` once cleared. */
|
|
762
|
+
lowBalanceAlertUsd: number | null;
|
|
763
|
+
}
|
|
755
764
|
interface WalletLedgerParams {
|
|
756
765
|
/** Default 50, max 200. */
|
|
757
766
|
limit?: number;
|
|
@@ -2029,6 +2038,18 @@ declare class Customers {
|
|
|
2029
2038
|
/** {@link adjustWallet}, addressing the customer by its `externalId` (`id`
|
|
2030
2039
|
* here). `POST /api/v1/customers/ext/:externalId/wallet/adjust`. */
|
|
2031
2040
|
adjustWalletByExternalId(id: string, input: WalletAdjustInput): Promise<AdjustResult>;
|
|
2041
|
+
/**
|
|
2042
|
+
* Set (a number of 0 or more, USD) or clear (`null`) a customer's low-balance
|
|
2043
|
+
* alert level. At or below it the `wallet.balance.drop` webhook fires for this
|
|
2044
|
+
* customer, whatever the endpoint's own threshold is; a customer without a
|
|
2045
|
+
* level falls back to each endpoint's threshold. Returns the saved level.
|
|
2046
|
+
* Idempotent (a PUT), so it retries safely. `PUT …/:id/wallet/alert`.
|
|
2047
|
+
*/
|
|
2048
|
+
setWalletAlert(id: string, threshold: number | null): Promise<WalletAlert>;
|
|
2049
|
+
/** {@link setWalletAlert}, addressing the customer by its `externalId` (`id`
|
|
2050
|
+
* here). `PUT /api/v1/customers/ext/:externalId/wallet/alert`. */
|
|
2051
|
+
setWalletAlertByExternalId(id: string, threshold: number | null): Promise<WalletAlert>;
|
|
2052
|
+
private walletAlert;
|
|
2032
2053
|
/**
|
|
2033
2054
|
* The one body behind all ten adjust calls.
|
|
2034
2055
|
*
|
|
@@ -2423,7 +2444,7 @@ declare class Signals {
|
|
|
2423
2444
|
*/
|
|
2424
2445
|
usageLog(params: UsageLogListParams): Promise<UsageLogPage>;
|
|
2425
2446
|
/**
|
|
2426
|
-
* Fetches up to
|
|
2447
|
+
* Fetches up to 10 usage-log rows by id, in one call.
|
|
2427
2448
|
* `GET /api/v1/signal/usage-log/lookup`.
|
|
2428
2449
|
*
|
|
2429
2450
|
* Built for linking a wallet debit to its usage: pass the `usageLogId`s
|
|
@@ -2432,7 +2453,7 @@ declare class Signals {
|
|
|
2432
2453
|
* carries its own `customerId`.
|
|
2433
2454
|
*
|
|
2434
2455
|
* Rows come back in the order of `ids`; duplicates are removed. Ids that
|
|
2435
|
-
* match nothing in your org are listed in `notFound`. More than
|
|
2456
|
+
* match nothing in your org are listed in `notFound`. More than 10
|
|
2436
2457
|
* distinct ids is a 400.
|
|
2437
2458
|
*/
|
|
2438
2459
|
lookupUsageLogs(ids: string[]): Promise<UsageLogLookupResult>;
|
|
@@ -2495,6 +2516,124 @@ declare class Taxes {
|
|
|
2495
2516
|
update(id: string, input: UpdateTaxInput): Promise<Tax>;
|
|
2496
2517
|
}
|
|
2497
2518
|
|
|
2519
|
+
/**
|
|
2520
|
+
* Receiving ClockNext webhooks.
|
|
2521
|
+
*
|
|
2522
|
+
* ClockNext POSTs a signed JSON event to each endpoint you register in the
|
|
2523
|
+
* dashboard (Settings → Webhooks). Two events exist today:
|
|
2524
|
+
*
|
|
2525
|
+
* wallet.balance.update a customer's MAIN wallet balance moved (credit or
|
|
2526
|
+
* debit). Invoice settlements and the Reserved
|
|
2527
|
+
* Wallet do not fire it.
|
|
2528
|
+
* wallet.balance.drop a debit took the balance from above a level to at
|
|
2529
|
+
* or below it. The level is the customer's own alert
|
|
2530
|
+
* level (`customers.setWalletAlert`) when they have
|
|
2531
|
+
* one, otherwise the endpoint's threshold. Fires once
|
|
2532
|
+
* per drop through the level, not on every debit while
|
|
2533
|
+
* the balance stays under it.
|
|
2534
|
+
*
|
|
2535
|
+
* Verify every request before trusting it — {@link constructWebhookEvent} does
|
|
2536
|
+
* that and returns the typed event. It needs the RAW request body: parsing the
|
|
2537
|
+
* JSON and re-serialising it changes the bytes and the signature will not match.
|
|
2538
|
+
*
|
|
2539
|
+
* Delivery is at-least-once: a non-2xx answer (or a timeout past 5s) is retried
|
|
2540
|
+
* with the SAME event `id`, so de-duplicate on `id`.
|
|
2541
|
+
*/
|
|
2542
|
+
/** Events ClockNext sends today. New event types may be added later — ignore
|
|
2543
|
+
* any `event_type` you do not recognise (and still answer 2xx). */
|
|
2544
|
+
type WebhookEventType = "wallet.balance.update" | "wallet.balance.drop";
|
|
2545
|
+
/**
|
|
2546
|
+
* The `source` a `wallet.balance.update` event carries: the STORED ledger
|
|
2547
|
+
* source, which is coarser than `WalletTransactionSource` on the ledger API.
|
|
2548
|
+
* Do not compare the two — the ledger API reports `SUBSCRIPTION`, `RENEWAL`,
|
|
2549
|
+
* `API`, … where the webhook says `TOPUP` or `ADJUSTMENT`.
|
|
2550
|
+
*
|
|
2551
|
+
* `SETTLE` is part of the type but `wallet.balance.update` does not fire for it.
|
|
2552
|
+
*/
|
|
2553
|
+
type WebhookWalletSource = "TOPUP" | "USAGE" | "SETTLE" | "ADJUSTMENT";
|
|
2554
|
+
/** The customer a wallet event is about. */
|
|
2555
|
+
interface WebhookCustomer {
|
|
2556
|
+
id: string;
|
|
2557
|
+
name: string;
|
|
2558
|
+
}
|
|
2559
|
+
/** Fields on every event. Field names are snake_case, exactly as sent. */
|
|
2560
|
+
interface WebhookEventBase<T extends WebhookEventType, D> {
|
|
2561
|
+
/** `evt_…`. Identical across retries of one delivery — de-duplicate on it. */
|
|
2562
|
+
id: string;
|
|
2563
|
+
event_type: T;
|
|
2564
|
+
/** ISO-8601 time the event was created. */
|
|
2565
|
+
triggered_at: string;
|
|
2566
|
+
data: D;
|
|
2567
|
+
}
|
|
2568
|
+
/** `data` of `wallet.balance.update`. Balances are USD. */
|
|
2569
|
+
interface WalletBalanceUpdateData {
|
|
2570
|
+
customer: WebhookCustomer;
|
|
2571
|
+
source: WebhookWalletSource;
|
|
2572
|
+
previous_balance: number;
|
|
2573
|
+
current_balance: number;
|
|
2574
|
+
currency_code: "usd";
|
|
2575
|
+
}
|
|
2576
|
+
/** `data` of `wallet.balance.drop`. Balances and `threshold` are USD. */
|
|
2577
|
+
interface WalletBalanceDropData {
|
|
2578
|
+
customer: WebhookCustomer;
|
|
2579
|
+
/** The level that was crossed: the customer's own alert level, else the
|
|
2580
|
+
* endpoint's threshold. */
|
|
2581
|
+
threshold: number;
|
|
2582
|
+
previous_balance: number;
|
|
2583
|
+
current_balance: number;
|
|
2584
|
+
currency_code: "usd";
|
|
2585
|
+
}
|
|
2586
|
+
type WalletBalanceUpdateEvent = WebhookEventBase<"wallet.balance.update", WalletBalanceUpdateData>;
|
|
2587
|
+
type WalletBalanceDropEvent = WebhookEventBase<"wallet.balance.drop", WalletBalanceDropData>;
|
|
2588
|
+
/** Any event ClockNext sends. Narrow on `event_type`. */
|
|
2589
|
+
type WebhookEvent = WalletBalanceUpdateEvent | WalletBalanceDropEvent;
|
|
2590
|
+
interface ConstructEventOptions {
|
|
2591
|
+
/** Reject an event whose signed timestamp is further than this from now, in
|
|
2592
|
+
* either direction (replay protection). Default 300 (5 minutes). Pass 0 to
|
|
2593
|
+
* skip the check — only do that in tests. */
|
|
2594
|
+
toleranceSeconds?: number;
|
|
2595
|
+
/** Override "now" (unix seconds). For tests. */
|
|
2596
|
+
now?: number;
|
|
2597
|
+
}
|
|
2598
|
+
/** Header ClockNext puts the signature in. Node lower-cases header names. */
|
|
2599
|
+
declare const WEBHOOK_SIGNATURE_HEADER = "x-clocknext-signature";
|
|
2600
|
+
/**
|
|
2601
|
+
* Signs a body the way ClockNext does: `t=<unix_seconds>,v1=<hex hmac>`, the
|
|
2602
|
+
* HMAC-SHA256 of `${timestamp}.${rawBody}` under the endpoint secret. Exported
|
|
2603
|
+
* so you can sign a fixture in your own receiver's tests.
|
|
2604
|
+
*/
|
|
2605
|
+
declare function signWebhookPayload(secret: string, rawBody: string | Buffer, timestamp: number): string;
|
|
2606
|
+
/**
|
|
2607
|
+
* Verifies a ClockNext webhook request and returns the typed event.
|
|
2608
|
+
*
|
|
2609
|
+
* ```ts
|
|
2610
|
+
* app.post("/clocknext", express.raw({ type: "application/json" }), (req, res) => {
|
|
2611
|
+
* let event;
|
|
2612
|
+
* try {
|
|
2613
|
+
* event = constructWebhookEvent(
|
|
2614
|
+
* req.body, // the raw Buffer, NOT parsed JSON
|
|
2615
|
+
* req.header("x-clocknext-signature"),
|
|
2616
|
+
* process.env.CLOCKNEXT_WEBHOOK_SECRET!,
|
|
2617
|
+
* );
|
|
2618
|
+
* } catch {
|
|
2619
|
+
* return res.sendStatus(400);
|
|
2620
|
+
* }
|
|
2621
|
+
* if (event.event_type === "wallet.balance.drop") { … }
|
|
2622
|
+
* res.sendStatus(200);
|
|
2623
|
+
* });
|
|
2624
|
+
* ```
|
|
2625
|
+
*
|
|
2626
|
+
* @throws {WebhookSignatureError} header missing or malformed, signature does
|
|
2627
|
+
* not match, timestamp outside the tolerance, or the body is not a JSON event.
|
|
2628
|
+
*/
|
|
2629
|
+
declare function constructWebhookEvent(rawBody: string | Buffer, signatureHeader: string | null | undefined, secret: string, options?: ConstructEventOptions): WebhookEvent;
|
|
2630
|
+
/** `cnk.webhooks` — the same verifier as {@link constructWebhookEvent}, for
|
|
2631
|
+
* callers who already hold a client. Needs no network and no API key use. */
|
|
2632
|
+
declare class Webhooks {
|
|
2633
|
+
/** See {@link constructWebhookEvent}. */
|
|
2634
|
+
constructEvent(rawBody: string | Buffer, signatureHeader: string | null | undefined, secret: string, options?: ConstructEventOptions): WebhookEvent;
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2498
2637
|
/**
|
|
2499
2638
|
* The ClockNext client.
|
|
2500
2639
|
*
|
|
@@ -2537,6 +2676,8 @@ declare class ClockNext {
|
|
|
2537
2676
|
readonly payments: Payments;
|
|
2538
2677
|
/** Mint customer-portal embed tokens. */
|
|
2539
2678
|
readonly portal: Portal;
|
|
2679
|
+
/** Verify incoming ClockNext webhooks (`wallet.balance.update` / `.drop`). */
|
|
2680
|
+
readonly webhooks: Webhooks;
|
|
2540
2681
|
private readonly cfg;
|
|
2541
2682
|
private readonly flusher;
|
|
2542
2683
|
constructor(config: ClockNextConfig);
|
|
@@ -2556,7 +2697,7 @@ declare class ClockNext {
|
|
|
2556
2697
|
*/
|
|
2557
2698
|
/** Machine-readable failure code, when the server sends one
|
|
2558
2699
|
* (`statusDetail.code`). Preferred over any message heuristic. */
|
|
2559
|
-
type ClockNextErrorCode = "ALLOWANCE_EXHAUSTED" | "PLAN_REJECTED" | "IDEMPOTENCY_UNRESOLVED" | (string & {});
|
|
2700
|
+
type ClockNextErrorCode = "ALLOWANCE_EXHAUSTED" | "PLAN_REJECTED" | "IDEMPOTENCY_UNRESOLVED" | "WEBHOOK_SIGNATURE_INVALID" | (string & {});
|
|
2560
2701
|
declare class ClockNextError extends Error {
|
|
2561
2702
|
/** HTTP status, when the failure came from a server response. */
|
|
2562
2703
|
readonly status?: number;
|
|
@@ -2617,6 +2758,12 @@ declare class RateLimitError extends ClockNextError {
|
|
|
2617
2758
|
/** 5xx — server-side error. */
|
|
2618
2759
|
declare class ServerError extends ClockNextError {
|
|
2619
2760
|
}
|
|
2761
|
+
/** A webhook request failed verification (`webhooks.constructEvent`): missing or
|
|
2762
|
+
* malformed signature header, wrong secret or altered body, a timestamp outside
|
|
2763
|
+
* the tolerance, or a body that is not a ClockNext event. Answer 400. */
|
|
2764
|
+
declare class WebhookSignatureError extends ClockNextError {
|
|
2765
|
+
constructor(message: string);
|
|
2766
|
+
}
|
|
2620
2767
|
/** fetch threw / request timed out — request never completed. */
|
|
2621
2768
|
declare class NetworkError extends ClockNextError {
|
|
2622
2769
|
constructor(message: string, opts?: {
|
|
@@ -2624,4 +2771,4 @@ declare class NetworkError extends ClockNextError {
|
|
|
2624
2771
|
});
|
|
2625
2772
|
}
|
|
2626
2773
|
|
|
2627
|
-
export { type AddMemberInput, type AdjustResult, AllowanceError, type ApiCustomer, AuthError, type BillingCycle, type BillingMode, ClockNext, type ClockNextConfig, ClockNextError, type ClockNextErrorCode, type ComponentType, type Composite, type CompositeAdjustInput, type CompositeBalance, type CompositeCompletionInput, type CompositeCompletionResult, type CompositeItem, type CompositeTag, ConflictError, type CreateCompositeInput, type CreateCreditInput, type CreateCustomerInput, type CreateOutcomeInput, type CreatePlanInput, type CreatePurchaseInput, type CreateTaxInput, type CreateUnitInput, type Credit, type CreditAdjustInput, type CreditBalance, type CreditInput, type CreditSignal, type CustomerBalances, type CustomerList, type CustomerPlan, type CustomerPlanDetails, type CustomerProfileInput, type CustomerRef, type DropReason, type FetchLike, ForbiddenError, type Invoice, type InvoiceDetail, type InvoiceLineItem, type InvoiceListRow, type InvoiceParty, type InvoicePlanSnapshot, type InvoiceStatus, type Logger, type Member, type MemberProfile, type ModelBundleLine, type ModelBundleLineInput, NetworkError, NotFoundError, type Outcome, type OutcomeAdjustInput, type OutcomeBalance, type OutcomeCompletionInput, type OutcomeCompletionResult, type OutcomeInput, type OutcomeSignal, type OutcomeStep, type OutcomeStepInput, type OutcomeWithSteps, type PayLink, type PaymentDetail, type PaymentRow, type Plan, type PlanComponent, type PlanEntitlement, type PlanEntitlementInput, PlanError, type PlanKind, type PlanRef, type PlanType, type PortalToken, type PortalTokenInput, type Purchase, type PurchaseDetail, type PurchaseStatus, RateLimitError, type RecurringGrantInput, type RecurringGrantResult, type RemovedMember, type SerializedOutcomeAttribution, type SerializedRuleApplication, type SerializedUnitUsage, type SerializedUsageLog, ServerError, type Signal, type SignalDelivery, type SignalStatus, type SignalType, type Tax, type TaxBehavior, type Tokens, type TrackResult, type Unit, type UnitAdjustInput, type UnitBalance, type UnitInput, type UnitPricingType, type UnitTier, type UnitTrackResult, type UpdateCompositeInput, type UpdateCreditInput, type UpdateMemberInput, type UpdateOutcomeInput, type UpdatePlanInput, type UpdateTaxInput, type UpdateUnitInput, type UsageBalance, type UsageLogAppliedRule, type UsageLogEntry, type UsageLogListParams, type UsageLogLookupEntitlementType, type UsageLogLookupEntry, type UsageLogLookupResult, type UsageLogOutcomeAttribution, type UsageLogPage, ValidationError, type WalletAdjustInput, type WalletInput, type WalletLedgerPage, type WalletLedgerParams, type WalletPool, type WalletSignal, type WalletTransaction, type WalletTransactionSource, signalToWire };
|
|
2774
|
+
export { type AddMemberInput, type AdjustResult, AllowanceError, type ApiCustomer, AuthError, type BillingCycle, type BillingMode, ClockNext, type ClockNextConfig, ClockNextError, type ClockNextErrorCode, type ComponentType, type Composite, type CompositeAdjustInput, type CompositeBalance, type CompositeCompletionInput, type CompositeCompletionResult, type CompositeItem, type CompositeTag, ConflictError, type ConstructEventOptions, type CreateCompositeInput, type CreateCreditInput, type CreateCustomerInput, type CreateOutcomeInput, type CreatePlanInput, type CreatePurchaseInput, type CreateTaxInput, type CreateUnitInput, type Credit, type CreditAdjustInput, type CreditBalance, type CreditInput, type CreditSignal, type CustomerBalances, type CustomerList, type CustomerPlan, type CustomerPlanDetails, type CustomerProfileInput, type CustomerRef, type DropReason, type FetchLike, ForbiddenError, type Invoice, type InvoiceDetail, type InvoiceLineItem, type InvoiceListRow, type InvoiceParty, type InvoicePlanSnapshot, type InvoiceStatus, type Logger, type Member, type MemberProfile, type ModelBundleLine, type ModelBundleLineInput, NetworkError, NotFoundError, type Outcome, type OutcomeAdjustInput, type OutcomeBalance, type OutcomeCompletionInput, type OutcomeCompletionResult, type OutcomeInput, type OutcomeSignal, type OutcomeStep, type OutcomeStepInput, type OutcomeWithSteps, type PayLink, type PaymentDetail, type PaymentRow, type Plan, type PlanComponent, type PlanEntitlement, type PlanEntitlementInput, PlanError, type PlanKind, type PlanRef, type PlanType, type PortalToken, type PortalTokenInput, type Purchase, type PurchaseDetail, type PurchaseStatus, RateLimitError, type RecurringGrantInput, type RecurringGrantResult, type RemovedMember, type SerializedOutcomeAttribution, type SerializedRuleApplication, type SerializedUnitUsage, type SerializedUsageLog, ServerError, type Signal, type SignalDelivery, type SignalStatus, type SignalType, type Tax, type TaxBehavior, type Tokens, type TrackResult, type Unit, type UnitAdjustInput, type UnitBalance, type UnitInput, type UnitPricingType, type UnitTier, type UnitTrackResult, type UpdateCompositeInput, type UpdateCreditInput, type UpdateMemberInput, type UpdateOutcomeInput, type UpdatePlanInput, type UpdateTaxInput, type UpdateUnitInput, type UsageBalance, type UsageLogAppliedRule, type UsageLogEntry, type UsageLogListParams, type UsageLogLookupEntitlementType, type UsageLogLookupEntry, type UsageLogLookupResult, type UsageLogOutcomeAttribution, type UsageLogPage, ValidationError, WEBHOOK_SIGNATURE_HEADER, type WalletAdjustInput, type WalletAlert, type WalletBalanceDropData, type WalletBalanceDropEvent, type WalletBalanceUpdateData, type WalletBalanceUpdateEvent, type WalletInput, type WalletLedgerPage, type WalletLedgerParams, type WalletPool, type WalletSignal, type WalletTransaction, type WalletTransactionSource, type WebhookCustomer, type WebhookEvent, type WebhookEventType, WebhookSignatureError, type WebhookWalletSource, constructWebhookEvent, signWebhookPayload, signalToWire };
|
package/dist/index.d.ts
CHANGED
|
@@ -748,10 +748,19 @@ interface WalletLedgerPage {
|
|
|
748
748
|
/** Reserved Wallet balance, USD; `null` when the workspace hasn't enabled
|
|
749
749
|
* it. */
|
|
750
750
|
reservedBalanceUsd: number | null;
|
|
751
|
+
/** The customer's own low-balance alert level, USD; `null` when none is set.
|
|
752
|
+
* At or below it the `wallet.balance.drop` webhook fires for this customer.
|
|
753
|
+
* Set with `customers.setWalletAlert()`. */
|
|
754
|
+
lowBalanceAlertUsd: number | null;
|
|
751
755
|
transactions: WalletTransaction[];
|
|
752
756
|
/** Pass back as `cursor` for the next page; `null` on the last page. */
|
|
753
757
|
nextCursor: string | null;
|
|
754
758
|
}
|
|
759
|
+
/** Result of `customers.setWalletAlert()`. */
|
|
760
|
+
interface WalletAlert {
|
|
761
|
+
/** The saved low-balance alert level, USD; `null` once cleared. */
|
|
762
|
+
lowBalanceAlertUsd: number | null;
|
|
763
|
+
}
|
|
755
764
|
interface WalletLedgerParams {
|
|
756
765
|
/** Default 50, max 200. */
|
|
757
766
|
limit?: number;
|
|
@@ -2029,6 +2038,18 @@ declare class Customers {
|
|
|
2029
2038
|
/** {@link adjustWallet}, addressing the customer by its `externalId` (`id`
|
|
2030
2039
|
* here). `POST /api/v1/customers/ext/:externalId/wallet/adjust`. */
|
|
2031
2040
|
adjustWalletByExternalId(id: string, input: WalletAdjustInput): Promise<AdjustResult>;
|
|
2041
|
+
/**
|
|
2042
|
+
* Set (a number of 0 or more, USD) or clear (`null`) a customer's low-balance
|
|
2043
|
+
* alert level. At or below it the `wallet.balance.drop` webhook fires for this
|
|
2044
|
+
* customer, whatever the endpoint's own threshold is; a customer without a
|
|
2045
|
+
* level falls back to each endpoint's threshold. Returns the saved level.
|
|
2046
|
+
* Idempotent (a PUT), so it retries safely. `PUT …/:id/wallet/alert`.
|
|
2047
|
+
*/
|
|
2048
|
+
setWalletAlert(id: string, threshold: number | null): Promise<WalletAlert>;
|
|
2049
|
+
/** {@link setWalletAlert}, addressing the customer by its `externalId` (`id`
|
|
2050
|
+
* here). `PUT /api/v1/customers/ext/:externalId/wallet/alert`. */
|
|
2051
|
+
setWalletAlertByExternalId(id: string, threshold: number | null): Promise<WalletAlert>;
|
|
2052
|
+
private walletAlert;
|
|
2032
2053
|
/**
|
|
2033
2054
|
* The one body behind all ten adjust calls.
|
|
2034
2055
|
*
|
|
@@ -2423,7 +2444,7 @@ declare class Signals {
|
|
|
2423
2444
|
*/
|
|
2424
2445
|
usageLog(params: UsageLogListParams): Promise<UsageLogPage>;
|
|
2425
2446
|
/**
|
|
2426
|
-
* Fetches up to
|
|
2447
|
+
* Fetches up to 10 usage-log rows by id, in one call.
|
|
2427
2448
|
* `GET /api/v1/signal/usage-log/lookup`.
|
|
2428
2449
|
*
|
|
2429
2450
|
* Built for linking a wallet debit to its usage: pass the `usageLogId`s
|
|
@@ -2432,7 +2453,7 @@ declare class Signals {
|
|
|
2432
2453
|
* carries its own `customerId`.
|
|
2433
2454
|
*
|
|
2434
2455
|
* Rows come back in the order of `ids`; duplicates are removed. Ids that
|
|
2435
|
-
* match nothing in your org are listed in `notFound`. More than
|
|
2456
|
+
* match nothing in your org are listed in `notFound`. More than 10
|
|
2436
2457
|
* distinct ids is a 400.
|
|
2437
2458
|
*/
|
|
2438
2459
|
lookupUsageLogs(ids: string[]): Promise<UsageLogLookupResult>;
|
|
@@ -2495,6 +2516,124 @@ declare class Taxes {
|
|
|
2495
2516
|
update(id: string, input: UpdateTaxInput): Promise<Tax>;
|
|
2496
2517
|
}
|
|
2497
2518
|
|
|
2519
|
+
/**
|
|
2520
|
+
* Receiving ClockNext webhooks.
|
|
2521
|
+
*
|
|
2522
|
+
* ClockNext POSTs a signed JSON event to each endpoint you register in the
|
|
2523
|
+
* dashboard (Settings → Webhooks). Two events exist today:
|
|
2524
|
+
*
|
|
2525
|
+
* wallet.balance.update a customer's MAIN wallet balance moved (credit or
|
|
2526
|
+
* debit). Invoice settlements and the Reserved
|
|
2527
|
+
* Wallet do not fire it.
|
|
2528
|
+
* wallet.balance.drop a debit took the balance from above a level to at
|
|
2529
|
+
* or below it. The level is the customer's own alert
|
|
2530
|
+
* level (`customers.setWalletAlert`) when they have
|
|
2531
|
+
* one, otherwise the endpoint's threshold. Fires once
|
|
2532
|
+
* per drop through the level, not on every debit while
|
|
2533
|
+
* the balance stays under it.
|
|
2534
|
+
*
|
|
2535
|
+
* Verify every request before trusting it — {@link constructWebhookEvent} does
|
|
2536
|
+
* that and returns the typed event. It needs the RAW request body: parsing the
|
|
2537
|
+
* JSON and re-serialising it changes the bytes and the signature will not match.
|
|
2538
|
+
*
|
|
2539
|
+
* Delivery is at-least-once: a non-2xx answer (or a timeout past 5s) is retried
|
|
2540
|
+
* with the SAME event `id`, so de-duplicate on `id`.
|
|
2541
|
+
*/
|
|
2542
|
+
/** Events ClockNext sends today. New event types may be added later — ignore
|
|
2543
|
+
* any `event_type` you do not recognise (and still answer 2xx). */
|
|
2544
|
+
type WebhookEventType = "wallet.balance.update" | "wallet.balance.drop";
|
|
2545
|
+
/**
|
|
2546
|
+
* The `source` a `wallet.balance.update` event carries: the STORED ledger
|
|
2547
|
+
* source, which is coarser than `WalletTransactionSource` on the ledger API.
|
|
2548
|
+
* Do not compare the two — the ledger API reports `SUBSCRIPTION`, `RENEWAL`,
|
|
2549
|
+
* `API`, … where the webhook says `TOPUP` or `ADJUSTMENT`.
|
|
2550
|
+
*
|
|
2551
|
+
* `SETTLE` is part of the type but `wallet.balance.update` does not fire for it.
|
|
2552
|
+
*/
|
|
2553
|
+
type WebhookWalletSource = "TOPUP" | "USAGE" | "SETTLE" | "ADJUSTMENT";
|
|
2554
|
+
/** The customer a wallet event is about. */
|
|
2555
|
+
interface WebhookCustomer {
|
|
2556
|
+
id: string;
|
|
2557
|
+
name: string;
|
|
2558
|
+
}
|
|
2559
|
+
/** Fields on every event. Field names are snake_case, exactly as sent. */
|
|
2560
|
+
interface WebhookEventBase<T extends WebhookEventType, D> {
|
|
2561
|
+
/** `evt_…`. Identical across retries of one delivery — de-duplicate on it. */
|
|
2562
|
+
id: string;
|
|
2563
|
+
event_type: T;
|
|
2564
|
+
/** ISO-8601 time the event was created. */
|
|
2565
|
+
triggered_at: string;
|
|
2566
|
+
data: D;
|
|
2567
|
+
}
|
|
2568
|
+
/** `data` of `wallet.balance.update`. Balances are USD. */
|
|
2569
|
+
interface WalletBalanceUpdateData {
|
|
2570
|
+
customer: WebhookCustomer;
|
|
2571
|
+
source: WebhookWalletSource;
|
|
2572
|
+
previous_balance: number;
|
|
2573
|
+
current_balance: number;
|
|
2574
|
+
currency_code: "usd";
|
|
2575
|
+
}
|
|
2576
|
+
/** `data` of `wallet.balance.drop`. Balances and `threshold` are USD. */
|
|
2577
|
+
interface WalletBalanceDropData {
|
|
2578
|
+
customer: WebhookCustomer;
|
|
2579
|
+
/** The level that was crossed: the customer's own alert level, else the
|
|
2580
|
+
* endpoint's threshold. */
|
|
2581
|
+
threshold: number;
|
|
2582
|
+
previous_balance: number;
|
|
2583
|
+
current_balance: number;
|
|
2584
|
+
currency_code: "usd";
|
|
2585
|
+
}
|
|
2586
|
+
type WalletBalanceUpdateEvent = WebhookEventBase<"wallet.balance.update", WalletBalanceUpdateData>;
|
|
2587
|
+
type WalletBalanceDropEvent = WebhookEventBase<"wallet.balance.drop", WalletBalanceDropData>;
|
|
2588
|
+
/** Any event ClockNext sends. Narrow on `event_type`. */
|
|
2589
|
+
type WebhookEvent = WalletBalanceUpdateEvent | WalletBalanceDropEvent;
|
|
2590
|
+
interface ConstructEventOptions {
|
|
2591
|
+
/** Reject an event whose signed timestamp is further than this from now, in
|
|
2592
|
+
* either direction (replay protection). Default 300 (5 minutes). Pass 0 to
|
|
2593
|
+
* skip the check — only do that in tests. */
|
|
2594
|
+
toleranceSeconds?: number;
|
|
2595
|
+
/** Override "now" (unix seconds). For tests. */
|
|
2596
|
+
now?: number;
|
|
2597
|
+
}
|
|
2598
|
+
/** Header ClockNext puts the signature in. Node lower-cases header names. */
|
|
2599
|
+
declare const WEBHOOK_SIGNATURE_HEADER = "x-clocknext-signature";
|
|
2600
|
+
/**
|
|
2601
|
+
* Signs a body the way ClockNext does: `t=<unix_seconds>,v1=<hex hmac>`, the
|
|
2602
|
+
* HMAC-SHA256 of `${timestamp}.${rawBody}` under the endpoint secret. Exported
|
|
2603
|
+
* so you can sign a fixture in your own receiver's tests.
|
|
2604
|
+
*/
|
|
2605
|
+
declare function signWebhookPayload(secret: string, rawBody: string | Buffer, timestamp: number): string;
|
|
2606
|
+
/**
|
|
2607
|
+
* Verifies a ClockNext webhook request and returns the typed event.
|
|
2608
|
+
*
|
|
2609
|
+
* ```ts
|
|
2610
|
+
* app.post("/clocknext", express.raw({ type: "application/json" }), (req, res) => {
|
|
2611
|
+
* let event;
|
|
2612
|
+
* try {
|
|
2613
|
+
* event = constructWebhookEvent(
|
|
2614
|
+
* req.body, // the raw Buffer, NOT parsed JSON
|
|
2615
|
+
* req.header("x-clocknext-signature"),
|
|
2616
|
+
* process.env.CLOCKNEXT_WEBHOOK_SECRET!,
|
|
2617
|
+
* );
|
|
2618
|
+
* } catch {
|
|
2619
|
+
* return res.sendStatus(400);
|
|
2620
|
+
* }
|
|
2621
|
+
* if (event.event_type === "wallet.balance.drop") { … }
|
|
2622
|
+
* res.sendStatus(200);
|
|
2623
|
+
* });
|
|
2624
|
+
* ```
|
|
2625
|
+
*
|
|
2626
|
+
* @throws {WebhookSignatureError} header missing or malformed, signature does
|
|
2627
|
+
* not match, timestamp outside the tolerance, or the body is not a JSON event.
|
|
2628
|
+
*/
|
|
2629
|
+
declare function constructWebhookEvent(rawBody: string | Buffer, signatureHeader: string | null | undefined, secret: string, options?: ConstructEventOptions): WebhookEvent;
|
|
2630
|
+
/** `cnk.webhooks` — the same verifier as {@link constructWebhookEvent}, for
|
|
2631
|
+
* callers who already hold a client. Needs no network and no API key use. */
|
|
2632
|
+
declare class Webhooks {
|
|
2633
|
+
/** See {@link constructWebhookEvent}. */
|
|
2634
|
+
constructEvent(rawBody: string | Buffer, signatureHeader: string | null | undefined, secret: string, options?: ConstructEventOptions): WebhookEvent;
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2498
2637
|
/**
|
|
2499
2638
|
* The ClockNext client.
|
|
2500
2639
|
*
|
|
@@ -2537,6 +2676,8 @@ declare class ClockNext {
|
|
|
2537
2676
|
readonly payments: Payments;
|
|
2538
2677
|
/** Mint customer-portal embed tokens. */
|
|
2539
2678
|
readonly portal: Portal;
|
|
2679
|
+
/** Verify incoming ClockNext webhooks (`wallet.balance.update` / `.drop`). */
|
|
2680
|
+
readonly webhooks: Webhooks;
|
|
2540
2681
|
private readonly cfg;
|
|
2541
2682
|
private readonly flusher;
|
|
2542
2683
|
constructor(config: ClockNextConfig);
|
|
@@ -2556,7 +2697,7 @@ declare class ClockNext {
|
|
|
2556
2697
|
*/
|
|
2557
2698
|
/** Machine-readable failure code, when the server sends one
|
|
2558
2699
|
* (`statusDetail.code`). Preferred over any message heuristic. */
|
|
2559
|
-
type ClockNextErrorCode = "ALLOWANCE_EXHAUSTED" | "PLAN_REJECTED" | "IDEMPOTENCY_UNRESOLVED" | (string & {});
|
|
2700
|
+
type ClockNextErrorCode = "ALLOWANCE_EXHAUSTED" | "PLAN_REJECTED" | "IDEMPOTENCY_UNRESOLVED" | "WEBHOOK_SIGNATURE_INVALID" | (string & {});
|
|
2560
2701
|
declare class ClockNextError extends Error {
|
|
2561
2702
|
/** HTTP status, when the failure came from a server response. */
|
|
2562
2703
|
readonly status?: number;
|
|
@@ -2617,6 +2758,12 @@ declare class RateLimitError extends ClockNextError {
|
|
|
2617
2758
|
/** 5xx — server-side error. */
|
|
2618
2759
|
declare class ServerError extends ClockNextError {
|
|
2619
2760
|
}
|
|
2761
|
+
/** A webhook request failed verification (`webhooks.constructEvent`): missing or
|
|
2762
|
+
* malformed signature header, wrong secret or altered body, a timestamp outside
|
|
2763
|
+
* the tolerance, or a body that is not a ClockNext event. Answer 400. */
|
|
2764
|
+
declare class WebhookSignatureError extends ClockNextError {
|
|
2765
|
+
constructor(message: string);
|
|
2766
|
+
}
|
|
2620
2767
|
/** fetch threw / request timed out — request never completed. */
|
|
2621
2768
|
declare class NetworkError extends ClockNextError {
|
|
2622
2769
|
constructor(message: string, opts?: {
|
|
@@ -2624,4 +2771,4 @@ declare class NetworkError extends ClockNextError {
|
|
|
2624
2771
|
});
|
|
2625
2772
|
}
|
|
2626
2773
|
|
|
2627
|
-
export { type AddMemberInput, type AdjustResult, AllowanceError, type ApiCustomer, AuthError, type BillingCycle, type BillingMode, ClockNext, type ClockNextConfig, ClockNextError, type ClockNextErrorCode, type ComponentType, type Composite, type CompositeAdjustInput, type CompositeBalance, type CompositeCompletionInput, type CompositeCompletionResult, type CompositeItem, type CompositeTag, ConflictError, type CreateCompositeInput, type CreateCreditInput, type CreateCustomerInput, type CreateOutcomeInput, type CreatePlanInput, type CreatePurchaseInput, type CreateTaxInput, type CreateUnitInput, type Credit, type CreditAdjustInput, type CreditBalance, type CreditInput, type CreditSignal, type CustomerBalances, type CustomerList, type CustomerPlan, type CustomerPlanDetails, type CustomerProfileInput, type CustomerRef, type DropReason, type FetchLike, ForbiddenError, type Invoice, type InvoiceDetail, type InvoiceLineItem, type InvoiceListRow, type InvoiceParty, type InvoicePlanSnapshot, type InvoiceStatus, type Logger, type Member, type MemberProfile, type ModelBundleLine, type ModelBundleLineInput, NetworkError, NotFoundError, type Outcome, type OutcomeAdjustInput, type OutcomeBalance, type OutcomeCompletionInput, type OutcomeCompletionResult, type OutcomeInput, type OutcomeSignal, type OutcomeStep, type OutcomeStepInput, type OutcomeWithSteps, type PayLink, type PaymentDetail, type PaymentRow, type Plan, type PlanComponent, type PlanEntitlement, type PlanEntitlementInput, PlanError, type PlanKind, type PlanRef, type PlanType, type PortalToken, type PortalTokenInput, type Purchase, type PurchaseDetail, type PurchaseStatus, RateLimitError, type RecurringGrantInput, type RecurringGrantResult, type RemovedMember, type SerializedOutcomeAttribution, type SerializedRuleApplication, type SerializedUnitUsage, type SerializedUsageLog, ServerError, type Signal, type SignalDelivery, type SignalStatus, type SignalType, type Tax, type TaxBehavior, type Tokens, type TrackResult, type Unit, type UnitAdjustInput, type UnitBalance, type UnitInput, type UnitPricingType, type UnitTier, type UnitTrackResult, type UpdateCompositeInput, type UpdateCreditInput, type UpdateMemberInput, type UpdateOutcomeInput, type UpdatePlanInput, type UpdateTaxInput, type UpdateUnitInput, type UsageBalance, type UsageLogAppliedRule, type UsageLogEntry, type UsageLogListParams, type UsageLogLookupEntitlementType, type UsageLogLookupEntry, type UsageLogLookupResult, type UsageLogOutcomeAttribution, type UsageLogPage, ValidationError, type WalletAdjustInput, type WalletInput, type WalletLedgerPage, type WalletLedgerParams, type WalletPool, type WalletSignal, type WalletTransaction, type WalletTransactionSource, signalToWire };
|
|
2774
|
+
export { type AddMemberInput, type AdjustResult, AllowanceError, type ApiCustomer, AuthError, type BillingCycle, type BillingMode, ClockNext, type ClockNextConfig, ClockNextError, type ClockNextErrorCode, type ComponentType, type Composite, type CompositeAdjustInput, type CompositeBalance, type CompositeCompletionInput, type CompositeCompletionResult, type CompositeItem, type CompositeTag, ConflictError, type ConstructEventOptions, type CreateCompositeInput, type CreateCreditInput, type CreateCustomerInput, type CreateOutcomeInput, type CreatePlanInput, type CreatePurchaseInput, type CreateTaxInput, type CreateUnitInput, type Credit, type CreditAdjustInput, type CreditBalance, type CreditInput, type CreditSignal, type CustomerBalances, type CustomerList, type CustomerPlan, type CustomerPlanDetails, type CustomerProfileInput, type CustomerRef, type DropReason, type FetchLike, ForbiddenError, type Invoice, type InvoiceDetail, type InvoiceLineItem, type InvoiceListRow, type InvoiceParty, type InvoicePlanSnapshot, type InvoiceStatus, type Logger, type Member, type MemberProfile, type ModelBundleLine, type ModelBundleLineInput, NetworkError, NotFoundError, type Outcome, type OutcomeAdjustInput, type OutcomeBalance, type OutcomeCompletionInput, type OutcomeCompletionResult, type OutcomeInput, type OutcomeSignal, type OutcomeStep, type OutcomeStepInput, type OutcomeWithSteps, type PayLink, type PaymentDetail, type PaymentRow, type Plan, type PlanComponent, type PlanEntitlement, type PlanEntitlementInput, PlanError, type PlanKind, type PlanRef, type PlanType, type PortalToken, type PortalTokenInput, type Purchase, type PurchaseDetail, type PurchaseStatus, RateLimitError, type RecurringGrantInput, type RecurringGrantResult, type RemovedMember, type SerializedOutcomeAttribution, type SerializedRuleApplication, type SerializedUnitUsage, type SerializedUsageLog, ServerError, type Signal, type SignalDelivery, type SignalStatus, type SignalType, type Tax, type TaxBehavior, type Tokens, type TrackResult, type Unit, type UnitAdjustInput, type UnitBalance, type UnitInput, type UnitPricingType, type UnitTier, type UnitTrackResult, type UpdateCompositeInput, type UpdateCreditInput, type UpdateMemberInput, type UpdateOutcomeInput, type UpdatePlanInput, type UpdateTaxInput, type UpdateUnitInput, type UsageBalance, type UsageLogAppliedRule, type UsageLogEntry, type UsageLogListParams, type UsageLogLookupEntitlementType, type UsageLogLookupEntry, type UsageLogLookupResult, type UsageLogOutcomeAttribution, type UsageLogPage, ValidationError, WEBHOOK_SIGNATURE_HEADER, type WalletAdjustInput, type WalletAlert, type WalletBalanceDropData, type WalletBalanceDropEvent, type WalletBalanceUpdateData, type WalletBalanceUpdateEvent, type WalletInput, type WalletLedgerPage, type WalletLedgerParams, type WalletPool, type WalletSignal, type WalletTransaction, type WalletTransactionSource, type WebhookCustomer, type WebhookEvent, type WebhookEventType, WebhookSignatureError, type WebhookWalletSource, constructWebhookEvent, signWebhookPayload, signalToWire };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
2
|
+
|
|
1
3
|
// src/errors.ts
|
|
2
4
|
var ClockNextError = class extends Error {
|
|
3
5
|
/** HTTP status, when the failure came from a server response. */
|
|
@@ -54,6 +56,11 @@ var RateLimitError = class extends ClockNextError {
|
|
|
54
56
|
};
|
|
55
57
|
var ServerError = class extends ClockNextError {
|
|
56
58
|
};
|
|
59
|
+
var WebhookSignatureError = class extends ClockNextError {
|
|
60
|
+
constructor(message) {
|
|
61
|
+
super(message, { status: 400, retryable: false, code: "WEBHOOK_SIGNATURE_INVALID" });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
57
64
|
var NetworkError = class extends ClockNextError {
|
|
58
65
|
constructor(message, opts = {}) {
|
|
59
66
|
super(message, { retryable: true, idempotencyKey: opts.idempotencyKey });
|
|
@@ -331,7 +338,7 @@ function sleep(ms) {
|
|
|
331
338
|
}
|
|
332
339
|
|
|
333
340
|
// src/http.ts
|
|
334
|
-
var SDK_VERSION = "0.
|
|
341
|
+
var SDK_VERSION = "0.20.0" ;
|
|
335
342
|
var Transport = class {
|
|
336
343
|
constructor(cfg) {
|
|
337
344
|
this.cfg = cfg;
|
|
@@ -872,6 +879,31 @@ var Customers = class {
|
|
|
872
879
|
adjustWalletByExternalId(id, input) {
|
|
873
880
|
return this.adjust(`/api/v1/customers/ext/${encodeURIComponent(id)}/wallet/adjust`, input);
|
|
874
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* Set (a number of 0 or more, USD) or clear (`null`) a customer's low-balance
|
|
884
|
+
* alert level. At or below it the `wallet.balance.drop` webhook fires for this
|
|
885
|
+
* customer, whatever the endpoint's own threshold is; a customer without a
|
|
886
|
+
* level falls back to each endpoint's threshold. Returns the saved level.
|
|
887
|
+
* Idempotent (a PUT), so it retries safely. `PUT …/:id/wallet/alert`.
|
|
888
|
+
*/
|
|
889
|
+
setWalletAlert(id, threshold) {
|
|
890
|
+
return this.walletAlert(`/api/v1/customers/${encodeURIComponent(id)}/wallet/alert`, threshold);
|
|
891
|
+
}
|
|
892
|
+
/** {@link setWalletAlert}, addressing the customer by its `externalId` (`id`
|
|
893
|
+
* here). `PUT /api/v1/customers/ext/:externalId/wallet/alert`. */
|
|
894
|
+
setWalletAlertByExternalId(id, threshold) {
|
|
895
|
+
return this.walletAlert(
|
|
896
|
+
`/api/v1/customers/ext/${encodeURIComponent(id)}/wallet/alert`,
|
|
897
|
+
threshold
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
walletAlert(path, threshold) {
|
|
901
|
+
return this.transport.request({
|
|
902
|
+
method: "PUT",
|
|
903
|
+
path,
|
|
904
|
+
body: { threshold }
|
|
905
|
+
});
|
|
906
|
+
}
|
|
875
907
|
/**
|
|
876
908
|
* The one body behind all ten adjust calls.
|
|
877
909
|
*
|
|
@@ -1490,7 +1522,7 @@ var Signals = class {
|
|
|
1490
1522
|
});
|
|
1491
1523
|
}
|
|
1492
1524
|
/**
|
|
1493
|
-
* Fetches up to
|
|
1525
|
+
* Fetches up to 10 usage-log rows by id, in one call.
|
|
1494
1526
|
* `GET /api/v1/signal/usage-log/lookup`.
|
|
1495
1527
|
*
|
|
1496
1528
|
* Built for linking a wallet debit to its usage: pass the `usageLogId`s
|
|
@@ -1499,7 +1531,7 @@ var Signals = class {
|
|
|
1499
1531
|
* carries its own `customerId`.
|
|
1500
1532
|
*
|
|
1501
1533
|
* Rows come back in the order of `ids`; duplicates are removed. Ids that
|
|
1502
|
-
* match nothing in your org are listed in `notFound`. More than
|
|
1534
|
+
* match nothing in your org are listed in `notFound`. More than 10
|
|
1503
1535
|
* distinct ids is a 400.
|
|
1504
1536
|
*/
|
|
1505
1537
|
lookupUsageLogs(ids) {
|
|
@@ -1622,6 +1654,83 @@ var Taxes = class {
|
|
|
1622
1654
|
return res.tax;
|
|
1623
1655
|
}
|
|
1624
1656
|
};
|
|
1657
|
+
var WEBHOOK_SIGNATURE_HEADER = "x-clocknext-signature";
|
|
1658
|
+
var DEFAULT_TOLERANCE_SECONDS = 300;
|
|
1659
|
+
function signWebhookPayload(secret, rawBody, timestamp) {
|
|
1660
|
+
const hmac = createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex");
|
|
1661
|
+
return `t=${timestamp},v1=${hmac}`;
|
|
1662
|
+
}
|
|
1663
|
+
function parseHeader(header) {
|
|
1664
|
+
let t;
|
|
1665
|
+
const v1 = [];
|
|
1666
|
+
for (const part of header.split(",")) {
|
|
1667
|
+
const eq = part.indexOf("=");
|
|
1668
|
+
if (eq === -1) continue;
|
|
1669
|
+
const k = part.slice(0, eq).trim();
|
|
1670
|
+
const v = part.slice(eq + 1).trim();
|
|
1671
|
+
if (k === "t") {
|
|
1672
|
+
if (/^\d+$/.test(v)) t = Number(v);
|
|
1673
|
+
} else if (k === "v1") {
|
|
1674
|
+
v1.push(v);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
if (t === void 0 || v1.length === 0) {
|
|
1678
|
+
throw new WebhookSignatureError(
|
|
1679
|
+
"Malformed x-clocknext-signature header: expected `t=<unix_seconds>,v1=<hex>`."
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
return { t, v1 };
|
|
1683
|
+
}
|
|
1684
|
+
function safeEqualHex(expectedHex, givenHex) {
|
|
1685
|
+
const expected = Buffer.from(expectedHex, "hex");
|
|
1686
|
+
const given = Buffer.from(givenHex, "hex");
|
|
1687
|
+
return given.length === expected.length && timingSafeEqual(expected, given);
|
|
1688
|
+
}
|
|
1689
|
+
function constructWebhookEvent(rawBody, signatureHeader, secret, options = {}) {
|
|
1690
|
+
if (typeof rawBody !== "string" && !Buffer.isBuffer(rawBody)) {
|
|
1691
|
+
throw new WebhookSignatureError(
|
|
1692
|
+
"Webhook body must be the raw request body (string or Buffer), not parsed JSON. With Express use express.raw({ type: 'application/json' })."
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
if (!secret) {
|
|
1696
|
+
throw new WebhookSignatureError("A webhook secret is required.");
|
|
1697
|
+
}
|
|
1698
|
+
if (!signatureHeader) {
|
|
1699
|
+
throw new WebhookSignatureError("Missing x-clocknext-signature header.");
|
|
1700
|
+
}
|
|
1701
|
+
const { t, v1 } = parseHeader(signatureHeader);
|
|
1702
|
+
const expected = signWebhookPayload(secret, rawBody, t).split("v1=")[1];
|
|
1703
|
+
if (!v1.some((candidate) => safeEqualHex(expected, candidate))) {
|
|
1704
|
+
throw new WebhookSignatureError(
|
|
1705
|
+
"Webhook signature does not match. Check the secret and that the body is the unmodified raw body."
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
1709
|
+
if (tolerance > 0) {
|
|
1710
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
1711
|
+
if (Math.abs(now - t) > tolerance) {
|
|
1712
|
+
throw new WebhookSignatureError(
|
|
1713
|
+
`Webhook timestamp is outside the ${tolerance}s tolerance \u2014 possible replay.`
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
let parsed;
|
|
1718
|
+
try {
|
|
1719
|
+
parsed = JSON.parse(rawBody.toString());
|
|
1720
|
+
} catch {
|
|
1721
|
+
throw new WebhookSignatureError("Webhook body is not valid JSON.");
|
|
1722
|
+
}
|
|
1723
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.id !== "string" || typeof parsed.event_type !== "string") {
|
|
1724
|
+
throw new WebhookSignatureError("Webhook body is not a ClockNext event.");
|
|
1725
|
+
}
|
|
1726
|
+
return parsed;
|
|
1727
|
+
}
|
|
1728
|
+
var Webhooks = class {
|
|
1729
|
+
/** See {@link constructWebhookEvent}. */
|
|
1730
|
+
constructEvent(rawBody, signatureHeader, secret, options) {
|
|
1731
|
+
return constructWebhookEvent(rawBody, signatureHeader, secret, options);
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1625
1734
|
|
|
1626
1735
|
// src/client.ts
|
|
1627
1736
|
var ClockNext = class {
|
|
@@ -1649,6 +1758,8 @@ var ClockNext = class {
|
|
|
1649
1758
|
payments;
|
|
1650
1759
|
/** Mint customer-portal embed tokens. */
|
|
1651
1760
|
portal;
|
|
1761
|
+
/** Verify incoming ClockNext webhooks (`wallet.balance.update` / `.drop`). */
|
|
1762
|
+
webhooks;
|
|
1652
1763
|
cfg;
|
|
1653
1764
|
flusher;
|
|
1654
1765
|
constructor(config) {
|
|
@@ -1672,6 +1783,7 @@ var ClockNext = class {
|
|
|
1672
1783
|
this.invoices = new Invoices(transport);
|
|
1673
1784
|
this.payments = new Payments(transport);
|
|
1674
1785
|
this.portal = new Portal(transport);
|
|
1786
|
+
this.webhooks = new Webhooks();
|
|
1675
1787
|
}
|
|
1676
1788
|
/** Number of signals currently buffered (async mode). */
|
|
1677
1789
|
get pending() {
|
|
@@ -1687,6 +1799,6 @@ var ClockNext = class {
|
|
|
1687
1799
|
}
|
|
1688
1800
|
};
|
|
1689
1801
|
|
|
1690
|
-
export { AllowanceError, AuthError, ClockNext, ClockNextError, ConflictError, ForbiddenError, NetworkError, NotFoundError, PlanError, RateLimitError, ServerError, ValidationError, signalToWire };
|
|
1802
|
+
export { AllowanceError, AuthError, ClockNext, ClockNextError, ConflictError, ForbiddenError, NetworkError, NotFoundError, PlanError, RateLimitError, ServerError, ValidationError, WEBHOOK_SIGNATURE_HEADER, WebhookSignatureError, constructWebhookEvent, signWebhookPayload, signalToWire };
|
|
1691
1803
|
//# sourceMappingURL=index.js.map
|
|
1692
1804
|
//# sourceMappingURL=index.js.map
|