@vellumcharter/sdk 0.6.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Todd Esposito
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # @vellumcharter/sdk
2
+
3
+ Thin, dependency-free TypeScript client for the VellumCharter entitlements +
4
+ billing API. **Server-side only** — it carries a secret API key; never ship it to
5
+ a browser.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @vellumcharter/sdk
11
+ ```
12
+
13
+ Requires Node 18+ (uses the global `fetch`).
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { VellumCharterClient } from '@vellumcharter/sdk';
19
+
20
+ const vellum = new VellumCharterClient({
21
+ apiKey: process.env.VELLUM_API_KEY!, // vlm_<tenantId>.<secret>
22
+ tenantId: 'sideproj1',
23
+ cacheTtlMs: 30_000, // optional (default 30s; 0 disables)
24
+ // baseUrl defaults to https://api.vellumcharter.com
25
+ });
26
+
27
+ // Gate a feature:
28
+ if (await vellum.can('cust_123', 'exports')) {
29
+ // ...allow the export
30
+ }
31
+
32
+ // Or inspect the full set:
33
+ const ent = await vellum.getEntitlements('cust_123');
34
+ ent.isActive(); // true for active/trialing
35
+ ent.has('reports'); // boolean
36
+ ent.config<number>('max_users'); // typed config read
37
+ ent.found; // false when the customer has no record
38
+
39
+ // Start a Checkout Session (returns the hosted checkout URL to redirect to):
40
+ const { url } = await vellum.createCheckout({
41
+ accountId: 'acme',
42
+ planId: 'pro',
43
+ planVersion: 1,
44
+ seats: 3,
45
+ successUrl: 'https://app.example.com/billing/ok',
46
+ cancelUrl: 'https://app.example.com/billing/cancel',
47
+ });
48
+
49
+ // Drop a cached read once you know it changed:
50
+ vellum.invalidate('cust_123');
51
+ ```
52
+
53
+ Reads are cached with a short TTL (default 30s), and a customer with no
54
+ entitlements returns an empty, no-access set rather than throwing — so `can()`
55
+ and `has()` are always safe.
56
+
57
+ ## Billing facade
58
+
59
+ Uncached pass-throughs to the billing endpoints (the payment platform still
60
+ owns invoicing/proration/dunning). Shapes are typed (`SubscriptionView`,
61
+ `PaymentMethod`, `Invoice`) in the server's camelCase / ISO-timestamp form:
62
+
63
+ ```ts
64
+ const sub = await vellum.getSubscription('acme', 'unit_1'); // SubscriptionView
65
+ const methods = await vellum.listPaymentMethods('acme'); // PaymentMethod[]
66
+ const invoices = await vellum.listInvoices('acme', 'unit_1'); // Invoice[]
67
+ const pdf = await vellum.invoicePdfUrl('acme', 'in_123'); // hosted PDF URL
68
+
69
+ await vellum.setDefaultPaymentMethod('acme', 'pm_123');
70
+ await vellum.setSubscriptionPaymentMethod('acme', 'unit_1', 'pm_123');
71
+ await vellum.removePaymentMethod('acme', 'pm_123');
72
+ await vellum.cancelSubscription('acme', 'unit_1', /* atPeriodEnd */ true);
73
+ ```
74
+
75
+ ## Provisioning & subscriptions
76
+
77
+ Register accounts and customers, manage their status, and create subscriptions
78
+ server-side (no hosted page; the account must have a saved card):
79
+
80
+ ```ts
81
+ await vellum.createAccount({
82
+ accountId: 'acme',
83
+ email: 'billing@acme.test',
84
+ createBillingCustomer: true,
85
+ });
86
+ await vellum.createCustomer({ accountId: 'acme', customerId: 'unit_1', email: 'ada@acme.test' });
87
+ await vellum.setAccountStatus('acme', 'suspended');
88
+ await vellum.setCustomerStatus('acme', 'unit_1', 'disabled');
89
+
90
+ // Capture a card without charging, then subscribe directly:
91
+ const { url } = await vellum.createSetupCheckout({
92
+ accountId: 'acme',
93
+ successUrl: 'https://app.example.com/billing/ok',
94
+ cancelUrl: 'https://app.example.com/billing/cancel',
95
+ });
96
+ const { subscriptionId } = await vellum.createSubscription({
97
+ accountId: 'acme',
98
+ customerId: 'unit_1',
99
+ planId: 'pro',
100
+ planVersion: 1,
101
+ seats: 2,
102
+ });
103
+ ```
104
+
105
+ ## Credits
106
+
107
+ Prepaid, pay-as-you-go balances. `consumeCredits` is idempotent on your key and
108
+ all-or-nothing — it throws `VellumCharterApiError` with `status === 409` on an
109
+ insufficient balance (nothing is deducted):
110
+
111
+ ```ts
112
+ const { url } = await vellum.createCreditCheckout({
113
+ accountId: 'acme',
114
+ customerId: 'unit_1',
115
+ creditTypeId: 'render_minutes',
116
+ packId: 'pack_1000',
117
+ quantity: 2,
118
+ successUrl: 'https://app.example.com/credits/ok',
119
+ cancelUrl: 'https://app.example.com/credits/cancel',
120
+ });
121
+ const credits = await vellum.listCredits('unit_1'); // CreditBalance[]
122
+ const bal = await vellum.getCreditBalance('unit_1', 'render_minutes'); // CreditBalance
123
+ const r = await vellum.consumeCredits('unit_1', {
124
+ creditTypeId: 'render_minutes',
125
+ amount: 30,
126
+ idempotencyKey: 'render:job_42',
127
+ });
128
+ ```
129
+
130
+ ## Errors
131
+
132
+ `VellumCharterAuthError` on 401/403; `VellumCharterApiError` (with `status` and
133
+ `body`) on other non-2xx responses.
134
+
135
+ ## API reference
136
+
137
+ Full endpoint + webhook contract: https://api.vellumcharter.com/openapi.json
@@ -0,0 +1,244 @@
1
+ export interface Entitlements {
2
+ accountId?: string;
3
+ planId?: string;
4
+ planVersion?: number;
5
+ status?: string;
6
+ currentPeriodEnd?: string;
7
+ modules: string[];
8
+ config: Record<string, unknown>;
9
+ billingSubscriptionId?: string;
10
+ updatedAt?: string;
11
+ }
12
+ export interface HttpResponse {
13
+ ok: boolean;
14
+ status: number;
15
+ text(): Promise<string>;
16
+ /** Response headers; only needed to read `Location` on the invoice-PDF redirect. */
17
+ headers?: {
18
+ get(name: string): string | null;
19
+ };
20
+ }
21
+ export type FetchLike = (url: string, init?: {
22
+ method?: string;
23
+ headers?: Record<string, string>;
24
+ body?: string;
25
+ redirect?: 'follow' | 'manual' | 'error';
26
+ }) => Promise<HttpResponse>;
27
+ export declare class VellumCharterError extends Error {
28
+ }
29
+ export declare class VellumCharterAuthError extends VellumCharterError {
30
+ }
31
+ export declare class VellumCharterApiError extends VellumCharterError {
32
+ readonly status: number;
33
+ readonly body: string;
34
+ constructor(message: string, status: number, body: string);
35
+ }
36
+ export declare class EntitlementSet {
37
+ readonly data: Entitlements;
38
+ readonly found: boolean;
39
+ constructor(data: Entitlements, found: boolean);
40
+ get status(): string | undefined;
41
+ isActive(): boolean;
42
+ has(moduleId: string): boolean;
43
+ config<T = unknown>(key: string): T | undefined;
44
+ }
45
+ export interface VellumCharterClientOptions {
46
+ apiKey: string;
47
+ tenantId: string;
48
+ /** API root. Defaults to the production VellumCharter stack; override for tests/staging. */
49
+ baseUrl?: string;
50
+ /** Read cache TTL in ms (default 30000). Set 0 to disable. */
51
+ cacheTtlMs?: number;
52
+ fetch?: FetchLike;
53
+ }
54
+ export interface CheckoutParams {
55
+ accountId: string;
56
+ customerId: string;
57
+ planId: string;
58
+ planVersion: number;
59
+ seats?: number;
60
+ successUrl: string;
61
+ cancelUrl: string;
62
+ }
63
+ export interface SubscribeParams {
64
+ accountId: string;
65
+ customerId: string;
66
+ planId: string;
67
+ planVersion: number;
68
+ seats?: number;
69
+ }
70
+ export interface SetupCheckoutParams {
71
+ accountId: string;
72
+ successUrl: string;
73
+ cancelUrl: string;
74
+ }
75
+ export interface CreateAccountParams {
76
+ accountId: string;
77
+ name?: string;
78
+ email?: string;
79
+ /** Adopt an existing billing customer (e.g. migrating live data). */
80
+ billingCustomerId?: string;
81
+ /** Create a new billing customer (ignored when billingCustomerId is given). */
82
+ createBillingCustomer?: boolean;
83
+ }
84
+ export interface CreateCustomerParams {
85
+ accountId: string;
86
+ customerId: string;
87
+ email?: string;
88
+ }
89
+ export interface CreditCheckoutParams {
90
+ accountId: string;
91
+ customerId: string;
92
+ creditTypeId: string;
93
+ packId: string;
94
+ /** How many packs to buy at once (default 1); grants grantQty × quantity. */
95
+ quantity?: number;
96
+ successUrl: string;
97
+ cancelUrl: string;
98
+ }
99
+ export interface CreditBalance {
100
+ creditTypeId: string;
101
+ balance: number;
102
+ }
103
+ export interface ConsumeResult {
104
+ creditTypeId: string;
105
+ consumed: number;
106
+ balance: number;
107
+ /** True when the idempotencyKey was already applied (no new deduction). */
108
+ duplicate: boolean;
109
+ }
110
+ export interface PaymentMethod {
111
+ id: string;
112
+ brand?: string;
113
+ last4?: string;
114
+ expMonth?: number;
115
+ expYear?: number;
116
+ /** Cardholder name from the billing details, when set. */
117
+ name?: string;
118
+ isDefault: boolean;
119
+ }
120
+ export interface Invoice {
121
+ id: string;
122
+ number: string | null;
123
+ status: string | null;
124
+ amountDue: number;
125
+ amountPaid: number;
126
+ subtotal: number;
127
+ total: number;
128
+ tax: number | null;
129
+ paid: boolean;
130
+ currency: string;
131
+ created: string;
132
+ hostedInvoiceUrl: string | null;
133
+ invoicePdf: string | null;
134
+ }
135
+ export interface SubscriptionView {
136
+ id: string;
137
+ status: string;
138
+ cancelAtPeriodEnd: boolean;
139
+ seats?: number;
140
+ /** Recurring price per seat in the smallest currency unit (e.g. cents). */
141
+ amount?: number;
142
+ currency?: string;
143
+ /** Billing interval of the recurring price, e.g. `month` or `year`. */
144
+ interval?: string;
145
+ currentPeriodStart?: string;
146
+ currentPeriodEnd?: string;
147
+ /** When the trial ends (ISO), if the subscription is trialing. */
148
+ trialEnd?: string;
149
+ /** When the subscription was canceled (ISO), if it has been. */
150
+ canceledAt?: string;
151
+ /** The subscription's default payment-method id, when one is set. */
152
+ defaultPaymentMethod?: string;
153
+ }
154
+ export declare class VellumCharterClient {
155
+ private readonly baseUrl;
156
+ private readonly apiKey;
157
+ private readonly tenantId;
158
+ private readonly ttl;
159
+ private readonly doFetch;
160
+ private readonly cache;
161
+ constructor(opts: VellumCharterClientOptions);
162
+ getEntitlements(customerId: string): Promise<EntitlementSet>;
163
+ /** Shorthand: does the customer currently have access to a module? */
164
+ can(customerId: string, moduleId: string): Promise<boolean>;
165
+ /** Drop a cached read (e.g. after a webhook tells you it changed). */
166
+ invalidate(customerId?: string): void;
167
+ createCheckout(params: CheckoutParams): Promise<{
168
+ url: string;
169
+ }>;
170
+ /** Subscribe a customer server-side (no hosted page; the account must have a saved card). */
171
+ createSubscription(params: SubscribeParams): Promise<{
172
+ subscriptionId: string;
173
+ }>;
174
+ /** Create a setup-mode Checkout session to capture or update a card (no charge). */
175
+ createSetupCheckout(params: SetupCheckoutParams): Promise<{
176
+ url: string;
177
+ }>;
178
+ /** Create a billing account; optionally create or adopt its billing customer. */
179
+ createAccount(params: CreateAccountParams): Promise<{
180
+ accountId: string;
181
+ billingCustomerId?: string;
182
+ }>;
183
+ /** Create a customer (member) under an account. */
184
+ createCustomer(params: CreateCustomerParams): Promise<{
185
+ customerId: string;
186
+ }>;
187
+ /** Set an account's operational status. */
188
+ setAccountStatus(accountId: string, status: string): Promise<{
189
+ accountId: string;
190
+ status: string;
191
+ }>;
192
+ /** Set a customer's operational status. */
193
+ setCustomerStatus(accountId: string, customerId: string, status: string): Promise<{
194
+ customerId: string;
195
+ status: string;
196
+ }>;
197
+ /** The customer's current subscription. */
198
+ getSubscription(accountId: string, customerId: string): Promise<SubscriptionView>;
199
+ /** Cancel a customer's subscription, immediately or at period end. */
200
+ cancelSubscription(accountId: string, customerId: string, atPeriodEnd?: boolean): Promise<{
201
+ canceled: string;
202
+ atPeriodEnd: boolean;
203
+ }>;
204
+ /** The account's saved card payment methods. */
205
+ listPaymentMethods(accountId: string): Promise<PaymentMethod[]>;
206
+ /** Make a payment method the account's default for future invoices. */
207
+ setDefaultPaymentMethod(accountId: string, methodId: string): Promise<{
208
+ default: string;
209
+ }>;
210
+ /** Detach a payment method from the account. */
211
+ removePaymentMethod(accountId: string, methodId: string): Promise<{
212
+ removed: string;
213
+ }>;
214
+ /** Override the default payment method for one customer's subscription. */
215
+ setSubscriptionPaymentMethod(accountId: string, customerId: string, methodId: string): Promise<{
216
+ subscription: string;
217
+ default: string;
218
+ }>;
219
+ /** Invoices for the account, or scoped to one customer's subscription. */
220
+ listInvoices(accountId: string, customerId?: string): Promise<Invoice[]>;
221
+ /** Resolve the hosted-PDF URL for an invoice (follows the 302 `Location`). */
222
+ invoicePdfUrl(accountId: string, invoiceId: string): Promise<string>;
223
+ /** Create a one-time Checkout session to buy a credit pack for a customer. */
224
+ createCreditCheckout(params: CreditCheckoutParams): Promise<{
225
+ url: string;
226
+ }>;
227
+ /** All of a customer's prepaid credit balances. */
228
+ listCredits(customerId: string): Promise<CreditBalance[]>;
229
+ /** One credit balance (0 if the customer has never bought this type). */
230
+ getCreditBalance(customerId: string, creditTypeId: string): Promise<CreditBalance>;
231
+ /**
232
+ * Consume credits, all-or-nothing. Pass a stable `idempotencyKey` (e.g. the
233
+ * game id) so a retry is a safe no-op (returns the already-applied balance with
234
+ * `duplicate: true`). Throws VellumCharterApiError with `status === 409` when
235
+ * the balance is insufficient (nothing is deducted).
236
+ */
237
+ consumeCredits(customerId: string, params: {
238
+ creditTypeId: string;
239
+ amount: number;
240
+ idempotencyKey: string;
241
+ }): Promise<ConsumeResult>;
242
+ private billingFetch;
243
+ private parse;
244
+ }
package/dist/index.js ADDED
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+ // Thin HTTP client for consuming apps (server-side — it holds a secret API key).
3
+ // Wraps the entitlements read + checkout endpoints, adds the x-api-key header,
4
+ // and caches reads with a short TTL (the docs' pull-first model; resolves Q3).
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VellumCharterClient = exports.EntitlementSet = exports.VellumCharterApiError = exports.VellumCharterAuthError = exports.VellumCharterError = void 0;
7
+ class VellumCharterError extends Error {
8
+ }
9
+ exports.VellumCharterError = VellumCharterError;
10
+ class VellumCharterAuthError extends VellumCharterError {
11
+ }
12
+ exports.VellumCharterAuthError = VellumCharterAuthError;
13
+ class VellumCharterApiError extends VellumCharterError {
14
+ status;
15
+ body;
16
+ constructor(message, status, body) {
17
+ super(message);
18
+ this.status = status;
19
+ this.body = body;
20
+ }
21
+ }
22
+ exports.VellumCharterApiError = VellumCharterApiError;
23
+ const ACCESS_STATUSES = new Set(['active', 'trialing']);
24
+ // Ergonomic view over a customer's resolved entitlements. A customer with no
25
+ // record (404) yields an empty, no-access set, so gating never needs a null
26
+ // check: `set.has('exports')` is just false.
27
+ class EntitlementSet {
28
+ data;
29
+ found;
30
+ constructor(data, found) {
31
+ this.data = data;
32
+ this.found = found;
33
+ }
34
+ get status() {
35
+ return this.data.status;
36
+ }
37
+ isActive() {
38
+ return this.data.status !== undefined && ACCESS_STATUSES.has(this.data.status);
39
+ }
40
+ has(moduleId) {
41
+ return this.data.modules.includes(moduleId);
42
+ }
43
+ config(key) {
44
+ return this.data.config[key];
45
+ }
46
+ }
47
+ exports.EntitlementSet = EntitlementSet;
48
+ const DEFAULT_TTL_MS = 30_000;
49
+ const DEFAULT_BASE_URL = 'https://api.vellumcharter.com';
50
+ class VellumCharterClient {
51
+ baseUrl;
52
+ apiKey;
53
+ tenantId;
54
+ ttl;
55
+ doFetch;
56
+ cache = new Map();
57
+ constructor(opts) {
58
+ if (!opts.apiKey || !opts.tenantId) {
59
+ throw new VellumCharterError('apiKey and tenantId are required');
60
+ }
61
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
62
+ this.apiKey = opts.apiKey;
63
+ this.tenantId = opts.tenantId;
64
+ this.ttl = opts.cacheTtlMs ?? DEFAULT_TTL_MS;
65
+ this.doFetch =
66
+ opts.fetch ?? ((url, init) => fetch(url, init));
67
+ }
68
+ async getEntitlements(customerId) {
69
+ const cached = this.cache.get(customerId);
70
+ if (cached && cached.expiresAt > Date.now())
71
+ return cached.set;
72
+ const url = `${this.baseUrl}/tenants/${enc(this.tenantId)}/customers/${enc(customerId)}/entitlements`;
73
+ const res = await this.doFetch(url, { headers: { 'x-api-key': this.apiKey } });
74
+ let set;
75
+ if (res.status === 404) {
76
+ set = new EntitlementSet({ modules: [], config: {} }, false);
77
+ }
78
+ else {
79
+ set = new EntitlementSet(await this.parse(res), true);
80
+ }
81
+ if (this.ttl > 0)
82
+ this.cache.set(customerId, { set, expiresAt: Date.now() + this.ttl });
83
+ return set;
84
+ }
85
+ /** Shorthand: does the customer currently have access to a module? */
86
+ async can(customerId, moduleId) {
87
+ return (await this.getEntitlements(customerId)).has(moduleId);
88
+ }
89
+ /** Drop a cached read (e.g. after a webhook tells you it changed). */
90
+ invalidate(customerId) {
91
+ if (customerId)
92
+ this.cache.delete(customerId);
93
+ else
94
+ this.cache.clear();
95
+ }
96
+ async createCheckout(params) {
97
+ const { accountId, ...body } = params;
98
+ const url = `${this.baseUrl}/tenants/${enc(this.tenantId)}/accounts/${enc(accountId)}/checkout`;
99
+ const res = await this.doFetch(url, {
100
+ method: 'POST',
101
+ headers: { 'x-api-key': this.apiKey, 'content-type': 'application/json' },
102
+ body: JSON.stringify(body),
103
+ });
104
+ return this.parse(res);
105
+ }
106
+ /** Subscribe a customer server-side (no hosted page; the account must have a saved card). */
107
+ async createSubscription(params) {
108
+ const { accountId, customerId, ...body } = params;
109
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/customers/${enc(customerId)}/subscribe`, { method: 'POST', body: JSON.stringify(body) });
110
+ return this.parse(res);
111
+ }
112
+ /** Create a setup-mode Checkout session to capture or update a card (no charge). */
113
+ async createSetupCheckout(params) {
114
+ const { accountId, ...body } = params;
115
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/billing/setup-checkout`, {
116
+ method: 'POST',
117
+ body: JSON.stringify(body),
118
+ });
119
+ return this.parse(res);
120
+ }
121
+ // --- provisioning ---------------------------------------------------------
122
+ // Register accounts (orgs) and customers (members) and manage their lifecycle
123
+ // status. Create the account first, then its customers, then subscribe.
124
+ /** Create a billing account; optionally create or adopt its billing customer. */
125
+ async createAccount(params) {
126
+ const res = await this.billingFetch('/accounts', {
127
+ method: 'POST',
128
+ body: JSON.stringify(params),
129
+ });
130
+ return this.parse(res);
131
+ }
132
+ /** Create a customer (member) under an account. */
133
+ async createCustomer(params) {
134
+ const { accountId, ...body } = params;
135
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/customers`, {
136
+ method: 'POST',
137
+ body: JSON.stringify(body),
138
+ });
139
+ return this.parse(res);
140
+ }
141
+ /** Set an account's operational status. */
142
+ async setAccountStatus(accountId, status) {
143
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}`, {
144
+ method: 'PATCH',
145
+ body: JSON.stringify({ status }),
146
+ });
147
+ return this.parse(res);
148
+ }
149
+ /** Set a customer's operational status. */
150
+ async setCustomerStatus(accountId, customerId, status) {
151
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/customers/${enc(customerId)}`, { method: 'PATCH', body: JSON.stringify({ status }) });
152
+ return this.parse(res);
153
+ }
154
+ // --- billing facade -------------------------------------------------------
155
+ // Uncached pass-throughs to the billing endpoints (unlike getEntitlements).
156
+ // Shapes mirror the server Views; Stripe still owns invoicing/proration/dunning.
157
+ /** The customer's current subscription. */
158
+ async getSubscription(accountId, customerId) {
159
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/customers/${enc(customerId)}/subscription`);
160
+ return this.parse(res);
161
+ }
162
+ /** Cancel a customer's subscription, immediately or at period end. */
163
+ async cancelSubscription(accountId, customerId, atPeriodEnd = false) {
164
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/customers/${enc(customerId)}/subscription${atPeriodEnd ? '?atPeriodEnd=true' : ''}`, { method: 'DELETE' });
165
+ return this.parse(res);
166
+ }
167
+ /** The account's saved card payment methods. */
168
+ async listPaymentMethods(accountId) {
169
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/payment-methods`);
170
+ return (await this.parse(res)).methods;
171
+ }
172
+ /** Make a payment method the account's default for future invoices. */
173
+ async setDefaultPaymentMethod(accountId, methodId) {
174
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/payment-methods/${enc(methodId)}`, { method: 'PATCH' });
175
+ return this.parse(res);
176
+ }
177
+ /** Detach a payment method from the account. */
178
+ async removePaymentMethod(accountId, methodId) {
179
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/payment-methods/${enc(methodId)}`, { method: 'DELETE' });
180
+ return this.parse(res);
181
+ }
182
+ /** Override the default payment method for one customer's subscription. */
183
+ async setSubscriptionPaymentMethod(accountId, customerId, methodId) {
184
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/customers/${enc(customerId)}/payment-method`, { method: 'PATCH', body: JSON.stringify({ methodId }) });
185
+ return this.parse(res);
186
+ }
187
+ /** Invoices for the account, or scoped to one customer's subscription. */
188
+ async listInvoices(accountId, customerId) {
189
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/invoices${customerId ? `?customerId=${enc(customerId)}` : ''}`);
190
+ return (await this.parse(res)).invoices;
191
+ }
192
+ /** Resolve the hosted-PDF URL for an invoice (follows the 302 `Location`). */
193
+ async invoicePdfUrl(accountId, invoiceId) {
194
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/invoices/${enc(invoiceId)}/pdf`, { redirect: 'manual' });
195
+ if (res.status === 401 || res.status === 403) {
196
+ throw new VellumCharterAuthError(`VellumCharter auth failed (${res.status})`);
197
+ }
198
+ const location = res.headers?.get('location');
199
+ if (location)
200
+ return location;
201
+ throw new VellumCharterApiError(`VellumCharter API error (${res.status})`, res.status, await res.text());
202
+ }
203
+ // --- credits --------------------------------------------------------------
204
+ // Prepaid one-time-use credits. Uncached (a balance changes out-of-band via
205
+ // purchase/consume, unlike getEntitlements). consume is idempotent on the
206
+ // caller-supplied key and all-or-nothing.
207
+ /** Create a one-time Checkout session to buy a credit pack for a customer. */
208
+ async createCreditCheckout(params) {
209
+ const { accountId, ...body } = params;
210
+ const res = await this.billingFetch(`/accounts/${enc(accountId)}/credit-checkout`, {
211
+ method: 'POST',
212
+ body: JSON.stringify(body),
213
+ });
214
+ return this.parse(res);
215
+ }
216
+ /** All of a customer's prepaid credit balances. */
217
+ async listCredits(customerId) {
218
+ const res = await this.billingFetch(`/customers/${enc(customerId)}/credits`);
219
+ return (await this.parse(res)).credits;
220
+ }
221
+ /** One credit balance (0 if the customer has never bought this type). */
222
+ async getCreditBalance(customerId, creditTypeId) {
223
+ const res = await this.billingFetch(`/customers/${enc(customerId)}/credits/${enc(creditTypeId)}`);
224
+ return this.parse(res);
225
+ }
226
+ /**
227
+ * Consume credits, all-or-nothing. Pass a stable `idempotencyKey` (e.g. the
228
+ * game id) so a retry is a safe no-op (returns the already-applied balance with
229
+ * `duplicate: true`). Throws VellumCharterApiError with `status === 409` when
230
+ * the balance is insufficient (nothing is deducted).
231
+ */
232
+ async consumeCredits(customerId, params) {
233
+ const { creditTypeId, ...body } = params;
234
+ const res = await this.billingFetch(`/customers/${enc(customerId)}/credits/${enc(creditTypeId)}/consume`, { method: 'POST', body: JSON.stringify(body) });
235
+ return this.parse(res);
236
+ }
237
+ billingFetch(path, init) {
238
+ const headers = { 'x-api-key': this.apiKey };
239
+ if (init?.body)
240
+ headers['content-type'] = 'application/json';
241
+ return this.doFetch(`${this.baseUrl}/tenants/${enc(this.tenantId)}${path}`, {
242
+ ...init,
243
+ headers,
244
+ });
245
+ }
246
+ async parse(res) {
247
+ if (res.status === 401 || res.status === 403) {
248
+ throw new VellumCharterAuthError(`VellumCharter auth failed (${res.status})`);
249
+ }
250
+ const text = await res.text();
251
+ if (!res.ok)
252
+ throw new VellumCharterApiError(`VellumCharter API error (${res.status})`, res.status, text);
253
+ return JSON.parse(text);
254
+ }
255
+ }
256
+ exports.VellumCharterClient = VellumCharterClient;
257
+ const enc = encodeURIComponent;
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@vellumcharter/sdk",
3
+ "version": "0.6.0",
4
+ "description": "Thin HTTP client for the VellumCharter entitlements + billing API",
5
+ "license": "MIT",
6
+ "author": "Todd Esposito",
7
+ "homepage": "https://vellumcharter.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/tdesposito/VellumCharter.git",
11
+ "directory": "sdks/js"
12
+ },
13
+ "keywords": ["vellum", "entitlements", "billing", "subscriptions", "saas"],
14
+ "main": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "files": ["dist"],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.build.json",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "node --require ts-node/register --test test/*.test.ts",
24
+ "example": "ts-node examples/quickstart.ts",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22",
29
+ "ts-node": "^10",
30
+ "typescript": "^5"
31
+ }
32
+ }