mbase-sdk 0.0.2
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 +21 -0
- package/README.md +497 -0
- package/dist/index.cjs +714 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +717 -0
- package/dist/index.d.ts +717 -0
- package/dist/index.js +698 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var MeterbaseError = class extends Error {
|
|
3
|
+
constructor(message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var APIError = class extends MeterbaseError {
|
|
9
|
+
status;
|
|
10
|
+
code;
|
|
11
|
+
/** From `X-Request-Id`, when the engine sends one. */
|
|
12
|
+
requestId;
|
|
13
|
+
body;
|
|
14
|
+
constructor(args) {
|
|
15
|
+
super(args.message);
|
|
16
|
+
this.status = args.status;
|
|
17
|
+
this.code = args.code;
|
|
18
|
+
this.requestId = args.requestId;
|
|
19
|
+
this.body = args.body;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var AuthenticationError = class extends APIError {
|
|
23
|
+
};
|
|
24
|
+
var PermissionDeniedError = class extends APIError {
|
|
25
|
+
};
|
|
26
|
+
var NotFoundError = class extends APIError {
|
|
27
|
+
};
|
|
28
|
+
var ConflictError = class extends APIError {
|
|
29
|
+
};
|
|
30
|
+
var IdempotencyConflictError = class extends ConflictError {
|
|
31
|
+
/** The event that key already recorded. Undefined only if the engine sent a
|
|
32
|
+
* body this could not be read out of. */
|
|
33
|
+
existingEventId;
|
|
34
|
+
constructor(args) {
|
|
35
|
+
super(args);
|
|
36
|
+
this.existingEventId = existingEventId(args.body);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var InvalidRequestError = class extends APIError {
|
|
40
|
+
};
|
|
41
|
+
var CycleChangeRequiresResetError = class extends InvalidRequestError {
|
|
42
|
+
};
|
|
43
|
+
var RateLimitError = class extends APIError {
|
|
44
|
+
/** Seconds to wait, from `Retry-After`, when the engine sends one. */
|
|
45
|
+
retryAfter;
|
|
46
|
+
constructor(args) {
|
|
47
|
+
super(args);
|
|
48
|
+
this.retryAfter = args.retryAfter;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var ServerError = class extends APIError {
|
|
52
|
+
};
|
|
53
|
+
var ConnectionError = class extends MeterbaseError {
|
|
54
|
+
};
|
|
55
|
+
var TimeoutError = class extends ConnectionError {
|
|
56
|
+
};
|
|
57
|
+
function errorFromResponse(args) {
|
|
58
|
+
switch (args.status) {
|
|
59
|
+
case 401:
|
|
60
|
+
return new AuthenticationError(args);
|
|
61
|
+
case 403:
|
|
62
|
+
return new PermissionDeniedError(args);
|
|
63
|
+
case 404:
|
|
64
|
+
return new NotFoundError(args);
|
|
65
|
+
case 409:
|
|
66
|
+
return args.code === "idempotency_conflict" ? new IdempotencyConflictError(args) : new ConflictError(args);
|
|
67
|
+
case 422:
|
|
68
|
+
return args.code === "cycle_change_requires_reset" ? new CycleChangeRequiresResetError(args) : new InvalidRequestError(args);
|
|
69
|
+
case 429:
|
|
70
|
+
return new RateLimitError(args);
|
|
71
|
+
default:
|
|
72
|
+
return args.status >= 500 ? new ServerError(args) : new APIError(args);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function existingEventId(body) {
|
|
76
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
77
|
+
const error = body.error;
|
|
78
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
79
|
+
const id = error.existing_event_id;
|
|
80
|
+
return typeof id === "string" ? id : void 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/client.ts
|
|
84
|
+
var DEFAULT_BASE_URL = "https://api.meterbase.tech";
|
|
85
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
86
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
87
|
+
var MAX_BACKOFF = 8e3;
|
|
88
|
+
var Client = class {
|
|
89
|
+
#apiKey;
|
|
90
|
+
#baseUrl;
|
|
91
|
+
#timeout;
|
|
92
|
+
#maxRetries;
|
|
93
|
+
#fetch;
|
|
94
|
+
constructor(options) {
|
|
95
|
+
if (!options.apiKey) {
|
|
96
|
+
throw new MeterbaseError(
|
|
97
|
+
"An API key is required: new Meterbase({ apiKey: 'mb_sk_live_\u2026' })."
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
this.#apiKey = options.apiKey;
|
|
101
|
+
this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
102
|
+
this.#timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
103
|
+
this.#maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
104
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
105
|
+
if (typeof this.#fetch !== "function") {
|
|
106
|
+
throw new MeterbaseError(
|
|
107
|
+
"No fetch implementation: pass one as `fetch`, or run on Node 18+."
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async request(req) {
|
|
112
|
+
const maxRetries = req.maxRetries ?? this.#maxRetries;
|
|
113
|
+
const retryable = req.method === "GET" || req.method === "DELETE" || req.idempotent === true;
|
|
114
|
+
let lastError;
|
|
115
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
116
|
+
if (attempt > 0) await sleep(backoff(attempt, lastError));
|
|
117
|
+
try {
|
|
118
|
+
const response = await this.#send(req);
|
|
119
|
+
if (response.ok) return await parseBody(response);
|
|
120
|
+
const error = await errorFrom(response);
|
|
121
|
+
if (retryable && attempt < maxRetries && shouldRetry(response.status)) {
|
|
122
|
+
lastError = error;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
} catch (cause) {
|
|
127
|
+
if (cause instanceof MeterbaseError && !(cause instanceof ConnectionError)) {
|
|
128
|
+
throw cause;
|
|
129
|
+
}
|
|
130
|
+
if (!retryable || attempt >= maxRetries) throw cause;
|
|
131
|
+
lastError = cause;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw lastError;
|
|
135
|
+
}
|
|
136
|
+
async #send(req) {
|
|
137
|
+
const url = new URL(this.#baseUrl + req.path);
|
|
138
|
+
for (const [key, value] of Object.entries(req.query ?? {})) {
|
|
139
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
140
|
+
}
|
|
141
|
+
const headers = {
|
|
142
|
+
authorization: `Bearer ${this.#apiKey}`,
|
|
143
|
+
accept: "application/json"
|
|
144
|
+
};
|
|
145
|
+
if (req.body !== void 0) headers["content-type"] = "application/json";
|
|
146
|
+
const timeout = req.timeout ?? this.#timeout;
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const onAbort = () => controller.abort(req.signal?.reason);
|
|
149
|
+
req.signal?.addEventListener("abort", onAbort, { once: true });
|
|
150
|
+
let timedOut = false;
|
|
151
|
+
const timer = setTimeout(() => {
|
|
152
|
+
timedOut = true;
|
|
153
|
+
controller.abort();
|
|
154
|
+
}, timeout);
|
|
155
|
+
try {
|
|
156
|
+
return await this.#fetch(url, {
|
|
157
|
+
method: req.method,
|
|
158
|
+
headers,
|
|
159
|
+
body: req.body === void 0 ? void 0 : JSON.stringify(req.body),
|
|
160
|
+
signal: controller.signal
|
|
161
|
+
});
|
|
162
|
+
} catch (cause) {
|
|
163
|
+
if (timedOut) {
|
|
164
|
+
throw new TimeoutError(`Request timed out after ${timeout}ms.`, {
|
|
165
|
+
cause
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (req.signal?.aborted) throw cause;
|
|
169
|
+
throw new ConnectionError(`Could not reach ${url.origin}.`, { cause });
|
|
170
|
+
} finally {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
req.signal?.removeEventListener("abort", onAbort);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
function shouldRetry(status) {
|
|
177
|
+
return status === 408 || status === 429 || status >= 500;
|
|
178
|
+
}
|
|
179
|
+
function backoff(attempt, lastError) {
|
|
180
|
+
const retryAfter = lastError && typeof lastError === "object" && "retryAfter" in lastError ? lastError.retryAfter : void 0;
|
|
181
|
+
if (retryAfter !== void 0) return Math.min(retryAfter * 1e3, MAX_BACKOFF);
|
|
182
|
+
const ceiling = Math.min(500 * 2 ** (attempt - 1), MAX_BACKOFF);
|
|
183
|
+
return Math.random() * ceiling;
|
|
184
|
+
}
|
|
185
|
+
function sleep(ms) {
|
|
186
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
187
|
+
}
|
|
188
|
+
async function parseBody(response) {
|
|
189
|
+
if (response.status === 204) return void 0;
|
|
190
|
+
const text = await response.text();
|
|
191
|
+
if (text === "") return void 0;
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(text);
|
|
194
|
+
} catch (cause) {
|
|
195
|
+
throw new MeterbaseError("The engine returned a body that is not JSON.", {
|
|
196
|
+
cause
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function errorFrom(response) {
|
|
201
|
+
const body = await response.text();
|
|
202
|
+
const parsed = parseErrorBody(body);
|
|
203
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
204
|
+
const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
|
|
205
|
+
return errorFromResponse({
|
|
206
|
+
status: response.status,
|
|
207
|
+
code: parsed?.code ?? `http_${response.status}`,
|
|
208
|
+
message: parsed?.message ?? `The engine responded ${response.status}.`,
|
|
209
|
+
requestId: response.headers.get("x-request-id") ?? void 0,
|
|
210
|
+
retryAfter: retryAfter !== void 0 && Number.isFinite(retryAfter) ? retryAfter : void 0,
|
|
211
|
+
body: body === "" ? void 0 : safeParse(body) ?? body
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function parseErrorBody(body) {
|
|
215
|
+
const parsed = safeParse(body);
|
|
216
|
+
const error = parsed?.error;
|
|
217
|
+
if (!error?.code || !error.message) return void 0;
|
|
218
|
+
return { code: error.code, message: error.message };
|
|
219
|
+
}
|
|
220
|
+
function safeParse(body) {
|
|
221
|
+
try {
|
|
222
|
+
return JSON.parse(body);
|
|
223
|
+
} catch {
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/resources/customer-allowances.ts
|
|
229
|
+
var CustomerAllowances = class {
|
|
230
|
+
#client;
|
|
231
|
+
constructor(client) {
|
|
232
|
+
this.#client = client;
|
|
233
|
+
}
|
|
234
|
+
grant(customerId, params, options) {
|
|
235
|
+
return this.#client.request({
|
|
236
|
+
...options,
|
|
237
|
+
method: "POST",
|
|
238
|
+
path: `/v1/customers/${encodeURIComponent(customerId)}/allowances`,
|
|
239
|
+
body: params
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/** Open grants only, unless `include_closed`. */
|
|
243
|
+
list(customerId, params = {}, options) {
|
|
244
|
+
return this.#client.request({
|
|
245
|
+
...options,
|
|
246
|
+
method: "GET",
|
|
247
|
+
path: `/v1/customers/${encodeURIComponent(customerId)}/allowances`,
|
|
248
|
+
query: {
|
|
249
|
+
meter_id: params.meter_id,
|
|
250
|
+
include_closed: params.include_closed
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
retrieve(customerId, allowanceId, options) {
|
|
255
|
+
return this.#client.request({
|
|
256
|
+
...options,
|
|
257
|
+
method: "GET",
|
|
258
|
+
path: `/v1/customers/${encodeURIComponent(customerId)}/allowances/${encodeURIComponent(allowanceId)}`
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/** Withdraws what is left without erasing what was consumed. Idempotent. */
|
|
262
|
+
revoke(customerId, allowanceId, options) {
|
|
263
|
+
return this.#client.request({
|
|
264
|
+
...options,
|
|
265
|
+
method: "DELETE",
|
|
266
|
+
path: `/v1/customers/${encodeURIComponent(customerId)}/allowances/${encodeURIComponent(allowanceId)}`
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/resources/customer-plan.ts
|
|
272
|
+
var CustomerPlan = class {
|
|
273
|
+
#client;
|
|
274
|
+
constructor(client) {
|
|
275
|
+
this.#client = client;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Put a customer on a plan. This is the wire call, and the only one that
|
|
279
|
+
* takes `cycle_anchor` — a first assignment's one chance to put their
|
|
280
|
+
* periods on a date they already have.
|
|
281
|
+
*
|
|
282
|
+
* Returns the instruction as recorded, which is not always as asked: a first
|
|
283
|
+
* plan is recorded `immediate` whatever `effective` said.
|
|
284
|
+
*/
|
|
285
|
+
assign(customerId, params, options) {
|
|
286
|
+
return this.#client.request({
|
|
287
|
+
...options,
|
|
288
|
+
method: "POST",
|
|
289
|
+
path: `/v1/customers/${encodeURIComponent(customerId)}/plan`,
|
|
290
|
+
body: params
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Move a customer to a different plan, with both knobs in the open.
|
|
295
|
+
* `effective` defaults to `next_cycle`; `reconciliation` applies only to an
|
|
296
|
+
* `immediate` change and defaults to `none`.
|
|
297
|
+
*
|
|
298
|
+
* Reach for `changeAtNextCycle`, `changeNow` or `restart` when one of them
|
|
299
|
+
* says what you mean — this is the form to fall back to when the two are
|
|
300
|
+
* chosen at runtime.
|
|
301
|
+
*/
|
|
302
|
+
change(customerId, params, options) {
|
|
303
|
+
const { plan, ...rest } = params;
|
|
304
|
+
return this.assign(customerId, { plan_id: plan, ...rest }, options);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Schedule the move for the end of the period they are in: they keep the
|
|
308
|
+
* plan they are paying for until it runs out, and the new one starts at
|
|
309
|
+
* their next boundary. Nothing is pro-rated, because nothing is split.
|
|
310
|
+
*
|
|
311
|
+
* Two plans on different cycles share no boundary, and this answers
|
|
312
|
+
* `CycleChangeRequiresResetError` — use `restart` for that move.
|
|
313
|
+
*/
|
|
314
|
+
changeAtNextCycle(customerId, params, options) {
|
|
315
|
+
return this.change(
|
|
316
|
+
customerId,
|
|
317
|
+
{ plan: params.plan, effective: "next_cycle" },
|
|
318
|
+
options
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Move them now, inside the period already running. Their renewal day does
|
|
323
|
+
* not move; what changes is this period's cap, and `reconciliation` says
|
|
324
|
+
* how:
|
|
325
|
+
*
|
|
326
|
+
* - `none` (the default) — the new plan's amount governs the whole period.
|
|
327
|
+
* Usage already spent still counts, so a downgrade can deny until the
|
|
328
|
+
* period ends.
|
|
329
|
+
* - `prorate` — each plan's amount weighted by the fraction of the period it
|
|
330
|
+
* was held for. Refused between plans on different cycles, which share no
|
|
331
|
+
* period to weight.
|
|
332
|
+
*
|
|
333
|
+
* To have the period itself start over instead, use `restart`.
|
|
334
|
+
*/
|
|
335
|
+
changeNow(customerId, params, options) {
|
|
336
|
+
return this.change(
|
|
337
|
+
customerId,
|
|
338
|
+
{ ...params, effective: "immediate" },
|
|
339
|
+
options
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Move them now and start a fresh period today: the period they were in
|
|
344
|
+
* closes where the change lands, keeping the usage it had, and the new
|
|
345
|
+
* plan's full amount opens immediately.
|
|
346
|
+
*
|
|
347
|
+
* This moves the customer's anchor, so their renewal day becomes today. It
|
|
348
|
+
* is the move for a customer starting over — a new contract, a re-signup —
|
|
349
|
+
* and the only one that can take a customer between plans whose cycles
|
|
350
|
+
* differ.
|
|
351
|
+
*/
|
|
352
|
+
restart(customerId, params, options) {
|
|
353
|
+
return this.change(
|
|
354
|
+
customerId,
|
|
355
|
+
{ plan: params.plan, effective: "immediate", reconciliation: "reset" },
|
|
356
|
+
options
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Call off a change that has not landed yet, leaving the customer on the
|
|
361
|
+
* plan they hold. Naming the plan already held is what supersedes a pending
|
|
362
|
+
* instruction, so this reads the plan in force and names it back.
|
|
363
|
+
*
|
|
364
|
+
* Returns the assignment still in force, or `null` for a customer holding no
|
|
365
|
+
* plan — who can have nothing pending, since a first assignment always lands
|
|
366
|
+
* at once.
|
|
367
|
+
*/
|
|
368
|
+
async cancelScheduledChange(customerId, options) {
|
|
369
|
+
const current = await this.retrieve(customerId, options);
|
|
370
|
+
if (current === null) return null;
|
|
371
|
+
return this.assign(customerId, { plan_id: current.plan_id }, options);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The plan in force now, which is not always the newest instruction: a
|
|
375
|
+
* change dated ahead does not govern yet.
|
|
376
|
+
*
|
|
377
|
+
* `null` when the customer holds no plan. That is a default deny rather than
|
|
378
|
+
* a failure — they are entitled to nothing — so it is an answer here and not
|
|
379
|
+
* a thrown `NotFoundError`. A customer who does not exist at all still
|
|
380
|
+
* throws one.
|
|
381
|
+
*/
|
|
382
|
+
async retrieve(customerId, options) {
|
|
383
|
+
try {
|
|
384
|
+
return await this.#client.request({
|
|
385
|
+
...options,
|
|
386
|
+
method: "GET",
|
|
387
|
+
path: `/v1/customers/${encodeURIComponent(customerId)}/plan`
|
|
388
|
+
});
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (error instanceof NotFoundError && error.code === "no_plan_assigned") {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* The change waiting to land, or `null` when none is. At most one is ever
|
|
398
|
+
* live: a newer instruction supersedes the one before it.
|
|
399
|
+
*
|
|
400
|
+
* Read against the assignment in force rather than against the local clock,
|
|
401
|
+
* so a change landing seconds from now is not reported as already governing.
|
|
402
|
+
*/
|
|
403
|
+
async pending(customerId, options) {
|
|
404
|
+
const [current, { data }] = await Promise.all([
|
|
405
|
+
this.retrieve(customerId, options),
|
|
406
|
+
this.history(customerId, options)
|
|
407
|
+
]);
|
|
408
|
+
const inForce = current === null ? -Infinity : Date.parse(current.effective_at);
|
|
409
|
+
return data.find(
|
|
410
|
+
(a) => a.superseded_at === null && Date.parse(a.effective_at) > inForce
|
|
411
|
+
) ?? null;
|
|
412
|
+
}
|
|
413
|
+
/** Every instruction, superseded ones included: it is the whole trail. */
|
|
414
|
+
history(customerId, options) {
|
|
415
|
+
return this.#client.request({
|
|
416
|
+
...options,
|
|
417
|
+
method: "GET",
|
|
418
|
+
path: `/v1/customers/${encodeURIComponent(customerId)}/plan/history`
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// src/resources/customers.ts
|
|
424
|
+
var Customers = class {
|
|
425
|
+
/** The plan they hold, and the trail of instructions that got them there. */
|
|
426
|
+
plan;
|
|
427
|
+
/** Grants, which sit on top of whatever the plan gives. */
|
|
428
|
+
allowances;
|
|
429
|
+
#client;
|
|
430
|
+
constructor(client) {
|
|
431
|
+
this.#client = client;
|
|
432
|
+
this.plan = new CustomerPlan(client);
|
|
433
|
+
this.allowances = new CustomerAllowances(client);
|
|
434
|
+
}
|
|
435
|
+
create(params, options) {
|
|
436
|
+
return this.#client.request({
|
|
437
|
+
...options,
|
|
438
|
+
method: "POST",
|
|
439
|
+
path: "/v1/customers",
|
|
440
|
+
body: params
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/** Lean: a listing does not embed the plan. Read one customer for that. */
|
|
444
|
+
list(params = {}, options) {
|
|
445
|
+
return this.#client.request({
|
|
446
|
+
...options,
|
|
447
|
+
method: "GET",
|
|
448
|
+
path: "/v1/customers",
|
|
449
|
+
query: { include_deleted: params.include_deleted }
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* One customer, with the plan they hold. `plan` is the one in force now and
|
|
454
|
+
* `pending_plan` the change waiting to land, each `null` when there is
|
|
455
|
+
* none — so knowing what a customer is on costs no second request.
|
|
456
|
+
*/
|
|
457
|
+
retrieve(id, options) {
|
|
458
|
+
return this.#client.request({
|
|
459
|
+
...options,
|
|
460
|
+
method: "GET",
|
|
461
|
+
path: `/v1/customers/${encodeURIComponent(id)}`
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Resolves the tenant's own identifier, or null, embedding the plan the way
|
|
466
|
+
* `retrieve` does. The engine answers this as a filtered list, so a miss is
|
|
467
|
+
* an empty collection rather than a 404.
|
|
468
|
+
*/
|
|
469
|
+
async retrieveByExternalId(externalId, options) {
|
|
470
|
+
const { data } = await this.#client.request({
|
|
471
|
+
...options,
|
|
472
|
+
method: "GET",
|
|
473
|
+
path: "/v1/customers",
|
|
474
|
+
query: { external_id: externalId }
|
|
475
|
+
});
|
|
476
|
+
return data[0] ?? null;
|
|
477
|
+
}
|
|
478
|
+
update(id, params, options) {
|
|
479
|
+
return this.#client.request({
|
|
480
|
+
...options,
|
|
481
|
+
method: "PATCH",
|
|
482
|
+
path: `/v1/customers/${encodeURIComponent(id)}`,
|
|
483
|
+
body: params
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
/** Soft delete: usage and assignments keep referencing the customer. */
|
|
487
|
+
delete(id, options) {
|
|
488
|
+
return this.#client.request({
|
|
489
|
+
...options,
|
|
490
|
+
method: "DELETE",
|
|
491
|
+
path: `/v1/customers/${encodeURIComponent(id)}`
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// src/resources/meters.ts
|
|
497
|
+
var Meters = class {
|
|
498
|
+
#client;
|
|
499
|
+
constructor(client) {
|
|
500
|
+
this.#client = client;
|
|
501
|
+
}
|
|
502
|
+
create(params, options) {
|
|
503
|
+
return this.#client.request({
|
|
504
|
+
...options,
|
|
505
|
+
method: "POST",
|
|
506
|
+
path: "/v1/meters",
|
|
507
|
+
body: params
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
list(params = {}, options) {
|
|
511
|
+
return this.#client.request({
|
|
512
|
+
...options,
|
|
513
|
+
method: "GET",
|
|
514
|
+
path: "/v1/meters",
|
|
515
|
+
query: { include_archived: params.include_archived }
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
retrieve(id, options) {
|
|
519
|
+
return this.#client.request({
|
|
520
|
+
...options,
|
|
521
|
+
method: "GET",
|
|
522
|
+
path: `/v1/meters/${encodeURIComponent(id)}`
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
update(id, params, options) {
|
|
526
|
+
return this.#client.request({
|
|
527
|
+
...options,
|
|
528
|
+
method: "PATCH",
|
|
529
|
+
path: `/v1/meters/${encodeURIComponent(id)}`,
|
|
530
|
+
body: params
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/** Soft delete: plans and usage keep referencing the meter. Idempotent. */
|
|
534
|
+
archive(id, options) {
|
|
535
|
+
return this.#client.request({
|
|
536
|
+
...options,
|
|
537
|
+
method: "DELETE",
|
|
538
|
+
path: `/v1/meters/${encodeURIComponent(id)}`
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// src/resources/plans.ts
|
|
544
|
+
var PlanAllowances = class {
|
|
545
|
+
#client;
|
|
546
|
+
constructor(client) {
|
|
547
|
+
this.#client = client;
|
|
548
|
+
}
|
|
549
|
+
set(planId, params, options) {
|
|
550
|
+
return this.#client.request({
|
|
551
|
+
...options,
|
|
552
|
+
method: "POST",
|
|
553
|
+
path: `/v1/plans/${encodeURIComponent(planId)}/allowances`,
|
|
554
|
+
body: params
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
/** Every version of every meter, newest first — history, not just current. */
|
|
558
|
+
list(planId, params = {}, options) {
|
|
559
|
+
return this.#client.request({
|
|
560
|
+
...options,
|
|
561
|
+
method: "GET",
|
|
562
|
+
path: `/v1/plans/${encodeURIComponent(planId)}/allowances`,
|
|
563
|
+
query: { meter_id: params.meter_id }
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
var Plans = class {
|
|
568
|
+
allowances;
|
|
569
|
+
#client;
|
|
570
|
+
constructor(client) {
|
|
571
|
+
this.#client = client;
|
|
572
|
+
this.allowances = new PlanAllowances(client);
|
|
573
|
+
}
|
|
574
|
+
/** A new plan grants nothing; attach entitlement with `allowances.set`. */
|
|
575
|
+
create(params, options) {
|
|
576
|
+
return this.#client.request({
|
|
577
|
+
...options,
|
|
578
|
+
method: "POST",
|
|
579
|
+
path: "/v1/plans",
|
|
580
|
+
body: params
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
list(params = {}, options) {
|
|
584
|
+
return this.#client.request({
|
|
585
|
+
...options,
|
|
586
|
+
method: "GET",
|
|
587
|
+
path: "/v1/plans",
|
|
588
|
+
query: { include_archived: params.include_archived }
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
retrieve(id, options) {
|
|
592
|
+
return this.#client.request({
|
|
593
|
+
...options,
|
|
594
|
+
method: "GET",
|
|
595
|
+
path: `/v1/plans/${encodeURIComponent(id)}`
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
update(id, params, options) {
|
|
599
|
+
return this.#client.request({
|
|
600
|
+
...options,
|
|
601
|
+
method: "PATCH",
|
|
602
|
+
path: `/v1/plans/${encodeURIComponent(id)}`,
|
|
603
|
+
body: params
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Stops new assignments without withdrawing capacity from anyone already on
|
|
608
|
+
* the plan, which is why an archived plan still resolves by id. Idempotent.
|
|
609
|
+
*/
|
|
610
|
+
archive(id, options) {
|
|
611
|
+
return this.#client.request({
|
|
612
|
+
...options,
|
|
613
|
+
method: "DELETE",
|
|
614
|
+
path: `/v1/plans/${encodeURIComponent(id)}`
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// src/index.ts
|
|
620
|
+
var Meterbase = class {
|
|
621
|
+
customers;
|
|
622
|
+
meters;
|
|
623
|
+
plans;
|
|
624
|
+
#client;
|
|
625
|
+
constructor(options) {
|
|
626
|
+
this.#client = new Client(options);
|
|
627
|
+
this.customers = new Customers(this.#client);
|
|
628
|
+
this.meters = new Meters(this.#client);
|
|
629
|
+
this.plans = new Plans(this.#client);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Asks whether a customer may consume `quantity` of a meter, named by the
|
|
633
|
+
* tenant's own external id and the meter's key. Absence of entitlement is
|
|
634
|
+
* not permission: a customer with no plan and no grant is denied.
|
|
635
|
+
*
|
|
636
|
+
* This is the gate, and the only call that ever refuses. Run it before the
|
|
637
|
+
* work; record the work with `track` afterwards.
|
|
638
|
+
*/
|
|
639
|
+
check(params, options) {
|
|
640
|
+
return this.#client.request({
|
|
641
|
+
...options,
|
|
642
|
+
method: "POST",
|
|
643
|
+
path: "/v1/check",
|
|
644
|
+
body: params
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Records usage that already happened — the tokens were spent, the image
|
|
649
|
+
* was generated — so it never refuses on capacity. A quantity beyond the
|
|
650
|
+
* customer's capacity is recorded, drives `available` to 0, and the next
|
|
651
|
+
* `check` says no. The answer is a receipt, not a verdict.
|
|
652
|
+
*
|
|
653
|
+
* `idempotency_key` is generated when omitted, so retrying this call — the
|
|
654
|
+
* SDK's own retries included — replays the original rather than counting
|
|
655
|
+
* the same work twice.
|
|
656
|
+
*/
|
|
657
|
+
// `async` so that generating the key cannot throw synchronously out of a
|
|
658
|
+
// method that otherwise only ever rejects: one call, one way to fail.
|
|
659
|
+
async track(params, options) {
|
|
660
|
+
return this.#client.request({
|
|
661
|
+
...options,
|
|
662
|
+
method: "POST",
|
|
663
|
+
path: "/v1/usage/track",
|
|
664
|
+
// Fixed here, once, before the retry loop is entered: every attempt of
|
|
665
|
+
// this call carries the same key, which is what makes replaying it safe.
|
|
666
|
+
body: {
|
|
667
|
+
...params,
|
|
668
|
+
idempotency_key: params.idempotency_key ?? idempotencyKey()
|
|
669
|
+
},
|
|
670
|
+
idempotent: true
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
/** Reports which workspace this key acts for. */
|
|
674
|
+
whoami(options) {
|
|
675
|
+
return this.#client.request({
|
|
676
|
+
...options,
|
|
677
|
+
method: "GET",
|
|
678
|
+
path: "/v1/whoami"
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
function idempotencyKey() {
|
|
683
|
+
const webcrypto = globalThis.crypto;
|
|
684
|
+
if (typeof webcrypto?.randomUUID === "function") {
|
|
685
|
+
return webcrypto.randomUUID();
|
|
686
|
+
}
|
|
687
|
+
if (typeof webcrypto?.getRandomValues === "function") {
|
|
688
|
+
const bytes = webcrypto.getRandomValues(new Uint8Array(16));
|
|
689
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
690
|
+
}
|
|
691
|
+
throw new MeterbaseError(
|
|
692
|
+
"No crypto to generate an idempotency key with: pass `idempotency_key` yourself, or run somewhere `crypto.getRandomValues` exists."
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export { APIError, AuthenticationError, ConflictError, ConnectionError, CycleChangeRequiresResetError, IdempotencyConflictError, InvalidRequestError, Meterbase, MeterbaseError, NotFoundError, PermissionDeniedError, RateLimitError, ServerError, TimeoutError, errorFromResponse };
|
|
697
|
+
//# sourceMappingURL=index.js.map
|
|
698
|
+
//# sourceMappingURL=index.js.map
|