@formata/limitr 0.5.13
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 +145 -0
- package/dist/gate.d.ts +22 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +79 -0
- package/dist/gate.js.map +1 -0
- package/dist/limitr.d.ts +2 -0
- package/dist/limitr.d.ts.map +1 -0
- package/dist/limitr.js +2 -0
- package/dist/limitr.js.map +1 -0
- package/dist/main.d.ts +296 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +746 -0
- package/dist/main.js.map +1 -0
- package/package.json +40 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 Formata, Inc. All rights reserved.
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
//
|
|
8
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
//
|
|
10
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
// See the License for the specific language governing permissions and
|
|
14
|
+
// limitations under the License.
|
|
15
|
+
//
|
|
16
|
+
import { StofDoc } from "@formata/stof";
|
|
17
|
+
import { limitrApi } from "./limitr.js";
|
|
18
|
+
import { LimitrGate, waitOnOpen } from "./gate.js";
|
|
19
|
+
/**
|
|
20
|
+
* Limitr monetization policy.
|
|
21
|
+
*/
|
|
22
|
+
export class Limitr {
|
|
23
|
+
/**
|
|
24
|
+
* Constructor.
|
|
25
|
+
* Make sure StofDoc.initialize() has been called for Stof first.
|
|
26
|
+
*/
|
|
27
|
+
constructor(policy = 'Limitr policy: {}', format = 'stof') {
|
|
28
|
+
/** Gate. */
|
|
29
|
+
this.gate = new LimitrGate();
|
|
30
|
+
/** Deny allows if cloud connection interrupted (recommended)? */
|
|
31
|
+
this.denyUnconnected = true;
|
|
32
|
+
this.wsInit = false;
|
|
33
|
+
/** Optional named event handlers for all Limitr events. */
|
|
34
|
+
this.eventHandlers = new Map();
|
|
35
|
+
/**
|
|
36
|
+
* Send on the cloud WebSocket if enabled.
|
|
37
|
+
*/
|
|
38
|
+
this._dataSendQueue = [];
|
|
39
|
+
/**
|
|
40
|
+
* Cloud message received.
|
|
41
|
+
*/
|
|
42
|
+
this._deniedCloudCustomers = new Set();
|
|
43
|
+
this.doc = new StofDoc();
|
|
44
|
+
this.doc.parse(limitrApi, 'bstf');
|
|
45
|
+
this.doc.parse(policy, format);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Add an event handler by name to this Limitr policy.
|
|
49
|
+
*/
|
|
50
|
+
addHandler(name, handler) {
|
|
51
|
+
this.eventHandlers.set(name, handler);
|
|
52
|
+
this.doc.lib('App', 'event_handler', (key, value) => {
|
|
53
|
+
for (const [_, handler] of this.eventHandlers)
|
|
54
|
+
handler(key, value);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Remove an event handler by name.
|
|
59
|
+
*/
|
|
60
|
+
removeHandler(name) {
|
|
61
|
+
return this.eventHandlers.delete(name);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Clear event handlers.
|
|
65
|
+
*/
|
|
66
|
+
clearHandlers() {
|
|
67
|
+
this.eventHandlers = new Map();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Async constructor that ensures Stof wasm initialization.
|
|
71
|
+
*/
|
|
72
|
+
static async new(policy = 'Limitr policy: {}', format = 'stof') {
|
|
73
|
+
await StofDoc.initialize();
|
|
74
|
+
return new Limitr(policy, format);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Call a specific Stof function in this doc by path/name through the gate.
|
|
78
|
+
* For complex use-cases, create your own StofDoc.
|
|
79
|
+
*/
|
|
80
|
+
async docCall(path, ...args) {
|
|
81
|
+
return await this.gate.run(() => this.doc.call(path, ...args));
|
|
82
|
+
}
|
|
83
|
+
/*****************************************************************************
|
|
84
|
+
* Plans API.
|
|
85
|
+
*****************************************************************************/
|
|
86
|
+
/**
|
|
87
|
+
* Get a plan record by ID (plan ID or customer ID).
|
|
88
|
+
*/
|
|
89
|
+
async plan(id, def = true) {
|
|
90
|
+
const planNode = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.plan', id, def));
|
|
91
|
+
if (typeof planNode === 'string')
|
|
92
|
+
return this.doc.record(planNode);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get the default plan if any.
|
|
97
|
+
*/
|
|
98
|
+
async defaultPlan() {
|
|
99
|
+
const planNode = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.default_plan'));
|
|
100
|
+
if (typeof planNode === 'string')
|
|
101
|
+
return this.doc.record(planNode);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get a plan price by ID (plan ID or customer ID).
|
|
106
|
+
*/
|
|
107
|
+
async planPrice(id) {
|
|
108
|
+
const priceNode = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.plan_price', id));
|
|
109
|
+
if (typeof priceNode === 'string')
|
|
110
|
+
return this.doc.record(priceNode);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get a plan price ID.
|
|
115
|
+
*/
|
|
116
|
+
async planPriceId(id) {
|
|
117
|
+
return await this.gate.run(() => this.doc.sync_call('<Limitr>.api.plan_price_id', id));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get a plan amount.
|
|
121
|
+
*/
|
|
122
|
+
async planAmount(id) {
|
|
123
|
+
return await this.gate.run(() => this.doc.sync_call('<Limitr>.api.plan_amount', id));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get a plan price label.
|
|
127
|
+
*/
|
|
128
|
+
async planPriceLabel(id) {
|
|
129
|
+
return await this.gate.run(() => this.doc.sync_call('<Limitr>.api.plan_price_label', id));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Set a plan on this policy by name/ID.
|
|
133
|
+
* Returns a node ID to the resulting Plan.
|
|
134
|
+
*/
|
|
135
|
+
async setPlan(id, planStof) {
|
|
136
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.set_plan', id, planStof));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Delete a plan by ID.
|
|
140
|
+
*/
|
|
141
|
+
async deletePlan(id) {
|
|
142
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.delete_plan', id));
|
|
143
|
+
}
|
|
144
|
+
/*****************************************************************************
|
|
145
|
+
* Credits API.
|
|
146
|
+
*****************************************************************************/
|
|
147
|
+
/**
|
|
148
|
+
* Get a credit record by ID/type.
|
|
149
|
+
*/
|
|
150
|
+
async credit(id) {
|
|
151
|
+
const node = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.credit', id));
|
|
152
|
+
if (typeof node === 'string')
|
|
153
|
+
return this.doc.record(node);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get a credit record for a specific entitlement.
|
|
158
|
+
* ID can be a plan ID or a customer ID.
|
|
159
|
+
*/
|
|
160
|
+
async creditFor(id, entitlement) {
|
|
161
|
+
const node = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.credit_for', id, entitlement));
|
|
162
|
+
if (typeof node === 'string')
|
|
163
|
+
return this.doc.record(node);
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
/*****************************************************************************
|
|
167
|
+
* Customers API.
|
|
168
|
+
*****************************************************************************/
|
|
169
|
+
/**
|
|
170
|
+
* Get a customer record by ID (or alternative IDs).
|
|
171
|
+
*/
|
|
172
|
+
async customer(id) {
|
|
173
|
+
const subNode = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.customer', id));
|
|
174
|
+
if (typeof subNode === 'string')
|
|
175
|
+
return this.doc.record(subNode);
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get a customer metadata object (if any).
|
|
180
|
+
*/
|
|
181
|
+
async customerMetadata(id) {
|
|
182
|
+
const metaNode = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.customer_metadata', id));
|
|
183
|
+
if (typeof metaNode === 'string')
|
|
184
|
+
return this.doc.record(metaNode);
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get all customers as a single record.
|
|
189
|
+
* Customers contain all state information, so this is all that is required to save/load.
|
|
190
|
+
*/
|
|
191
|
+
async customers() {
|
|
192
|
+
const node = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.get'));
|
|
193
|
+
if (typeof node === 'string') {
|
|
194
|
+
const subs = this.doc.get('customers', node);
|
|
195
|
+
if (typeof subs === 'string')
|
|
196
|
+
return this.doc.record(subs);
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get the customer reference IDs for a customer.
|
|
202
|
+
*/
|
|
203
|
+
async customerRefs(id) {
|
|
204
|
+
return await this.gate.run(() => this.doc.sync_call('<Limitr>.api.customer_refs', id));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Set/change a customer's plan by plan ID.
|
|
208
|
+
* Returns true if the plan has changed (and emits customer-set & customer-plan-changed events).
|
|
209
|
+
*/
|
|
210
|
+
async setCustomerPlan(id, plan, overwrite_meters = true) {
|
|
211
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.set_customer_plan', id, plan, overwrite_meters));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Ensure that a customer exists, creating one if necessary.
|
|
215
|
+
* This takes the cloud into consideration as well.
|
|
216
|
+
* Returns true if a new customer was created.
|
|
217
|
+
*/
|
|
218
|
+
async ensureCustomer(id, plan = '', type = 'user', label = 'User', refs = null, alts = null, metadata = null) {
|
|
219
|
+
const existing = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.customer', id));
|
|
220
|
+
if (existing)
|
|
221
|
+
return false;
|
|
222
|
+
if (this.ws) {
|
|
223
|
+
switch (this.ws.readyState) {
|
|
224
|
+
case WebSocket.OPEN: {
|
|
225
|
+
if (await this.addCloudCustomer(id))
|
|
226
|
+
return false;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case WebSocket.CONNECTING: {
|
|
230
|
+
await waitOnOpen(this.ws);
|
|
231
|
+
if (await this.addCloudCustomer(id))
|
|
232
|
+
return false;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case WebSocket.CLOSING:
|
|
236
|
+
case WebSocket.CLOSED: {
|
|
237
|
+
if (this.denyUnconnected)
|
|
238
|
+
return false;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
let meta = null;
|
|
244
|
+
if (typeof metadata === 'string')
|
|
245
|
+
meta = metadata;
|
|
246
|
+
else if (metadata)
|
|
247
|
+
meta = JSON.stringify(metadata);
|
|
248
|
+
await this.gate.run(() => this.doc.call('<Limitr>.api.create_customer', id, plan, type, label, refs, alts, meta));
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Create a new customer and add to this Limitr.
|
|
253
|
+
* Use a unique ID - can always add additional unique IDs with alts (Ex. Stripe customer ID, API key, etc.).
|
|
254
|
+
* Note: prefer ensureCustomer API in case this customer already exists.
|
|
255
|
+
*/
|
|
256
|
+
async createCustomer(id, plan = '', type = 'user', label = 'User', refs = null, alts = null, metadata = null) {
|
|
257
|
+
let meta = null;
|
|
258
|
+
if (typeof metadata === 'string')
|
|
259
|
+
meta = metadata;
|
|
260
|
+
else if (metadata)
|
|
261
|
+
meta = JSON.stringify(metadata);
|
|
262
|
+
await this.gate.run(() => this.doc.call('<Limitr>.api.create_customer', id, plan, type, label, refs, alts, meta));
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Ensure that a customer exists, creating one by record if necessary.
|
|
266
|
+
* This takes the cloud into consideration as well.
|
|
267
|
+
* Returns true if a new customer was created.
|
|
268
|
+
*/
|
|
269
|
+
async ensureSetCustomer(customer, event = true) {
|
|
270
|
+
const record = typeof customer === 'string' ? JSON.parse(customer) : customer;
|
|
271
|
+
const id = record.id;
|
|
272
|
+
if (!id)
|
|
273
|
+
throw new Error('Ensure setting a customer expects a customer record with an ID');
|
|
274
|
+
const existing = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.customer', id));
|
|
275
|
+
if (existing)
|
|
276
|
+
return false;
|
|
277
|
+
if (this.ws) {
|
|
278
|
+
switch (this.ws.readyState) {
|
|
279
|
+
case WebSocket.OPEN: {
|
|
280
|
+
if (await this.addCloudCustomer(id))
|
|
281
|
+
return false;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case WebSocket.CONNECTING: {
|
|
285
|
+
await waitOnOpen(this.ws);
|
|
286
|
+
if (await this.addCloudCustomer(id))
|
|
287
|
+
return false;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case WebSocket.CLOSING:
|
|
291
|
+
case WebSocket.CLOSED: {
|
|
292
|
+
if (this.denyUnconnected)
|
|
293
|
+
return false;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const res = await this.gate.run(() => this.doc.call('<Limitr>.api.set_customer', JSON.stringify(record), event));
|
|
299
|
+
return !!res;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Set a customer on this policy by ID.
|
|
303
|
+
* Returns a node ID to the resulting Customer.
|
|
304
|
+
*/
|
|
305
|
+
async setCustomer(customer, event = true) {
|
|
306
|
+
const customerStof = typeof customer === 'string' ? customer : JSON.stringify(customer);
|
|
307
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.set_customer', customerStof, event));
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Load many customers as records.
|
|
311
|
+
* This is all that is required for save/load since customers contain all state information.
|
|
312
|
+
*/
|
|
313
|
+
async loadCustomers(customers) {
|
|
314
|
+
let subs = [];
|
|
315
|
+
if (!Array.isArray(customers)) {
|
|
316
|
+
for (const [k, v] of Object.entries(customers)) {
|
|
317
|
+
const val = v;
|
|
318
|
+
val.id = k; // make sure it has the correct ID
|
|
319
|
+
subs.push(val);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
subs = customers;
|
|
324
|
+
}
|
|
325
|
+
const handles = [];
|
|
326
|
+
for (const sub of subs) {
|
|
327
|
+
const id = sub.id;
|
|
328
|
+
if (typeof id === 'string') {
|
|
329
|
+
handles.push(this.setCustomer(JSON.stringify(sub)));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
await Promise.allSettled(handles);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Remove a customer by ID.
|
|
336
|
+
* If a cloud customer, they will not be removed from the cloud.
|
|
337
|
+
*/
|
|
338
|
+
async removeCustomer(id) {
|
|
339
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.delete_customer', id));
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Add an alternative customer ID to an existing customer.
|
|
343
|
+
*/
|
|
344
|
+
async addAltID(existing, alt, event = true) {
|
|
345
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.set_alt_customer_id', existing, alt, event));
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Remove an alternative customer ID from an existing customer.
|
|
349
|
+
*/
|
|
350
|
+
async removeAltID(alt, event = true) {
|
|
351
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.delete_alt_customer_id', alt, event));
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Create a new customer override (limit).
|
|
355
|
+
* Overrides are specific to customers, and they override the limit defined on a specific entitlement for that customer.
|
|
356
|
+
*
|
|
357
|
+
* If the customer already has an override for this entitlement, it will be replaced.
|
|
358
|
+
*/
|
|
359
|
+
async createCustomerOverride(id, entitlement, value, expires_on, credit, mode, increment, resets, reset_inc) {
|
|
360
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.create_customer_override', id, entitlement, expires_on ?? null, credit ?? null, mode ?? null, value ?? null, increment ?? null, resets ?? null, reset_inc ?? null));
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Remove a customer override (limit).
|
|
364
|
+
*/
|
|
365
|
+
async removeCustomerOverride(id, entitlement) {
|
|
366
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.remove_customer_override', id, entitlement));
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Create a customer credit grant (recommended use in top-ups & one-time purchases).
|
|
370
|
+
* Grants are applied when overage would occur (soft entitlement limits).
|
|
371
|
+
* Defaults to a one-time, fixed-value credit grant.
|
|
372
|
+
* More than one grant with the same "credit" can exist alongside one another.
|
|
373
|
+
* Do not sync grants up with plans - if a plan includes a credit grant, just set a soft limit value (same thing).
|
|
374
|
+
*
|
|
375
|
+
* Ex. a soft limit of 5k tokens + a grant of 2k tokens would result in the customer
|
|
376
|
+
* getting overage events (meter-overage) after 7k tokens spent.
|
|
377
|
+
*
|
|
378
|
+
* @returns true when the customer & credit exists, meaning the grant has been applied.
|
|
379
|
+
*/
|
|
380
|
+
async create_customer_credit_grant(id, credit, value, resets = false, reset_inc, expires_on) {
|
|
381
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.create_customer_credit_grant', id, credit, value, resets, reset_inc ?? null, expires_on ?? null));
|
|
382
|
+
}
|
|
383
|
+
/*****************************************************************************
|
|
384
|
+
* Entitlements API.
|
|
385
|
+
*****************************************************************************/
|
|
386
|
+
/**
|
|
387
|
+
* Get an entitlement record with a plan ID or customer ID and an entitlement name.
|
|
388
|
+
*/
|
|
389
|
+
async entitlement(id, entitlement) {
|
|
390
|
+
const node = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.entitlement', id, entitlement));
|
|
391
|
+
if (typeof node === 'string')
|
|
392
|
+
return this.doc.record(node);
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Get the limit value for a metered entitlement.
|
|
397
|
+
* ID can be a customer ID or a plan ID (to get the specific entitlement from a plan).
|
|
398
|
+
* Will always be in the units of the credit associated with this entitlement (ex. limit.value = '2GB', credit.stof_units = 'MB', limit = 2000).
|
|
399
|
+
*/
|
|
400
|
+
async limit(id, entitlement) {
|
|
401
|
+
return await this.gate.run(() => this.doc.sync_call('<Limitr>.api.limit', id, entitlement));
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Get the remaining balance for a customer's entitlement (limit - current (metered) value).
|
|
405
|
+
* Will always be in the units of the credit associated with this entitlement.
|
|
406
|
+
*/
|
|
407
|
+
async remaining(customer, entitlement) {
|
|
408
|
+
if (!await this.cloudPreCheckContinue(customer))
|
|
409
|
+
return null;
|
|
410
|
+
return await this.gate.run(() => this.doc.sync_call('<Limitr>.api.remaining', customer, entitlement));
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get the current meter value for a customer's entitlement.
|
|
414
|
+
* Will always be in the units of the credit associated with this entitlement.
|
|
415
|
+
*/
|
|
416
|
+
async value(customer, entitlement) {
|
|
417
|
+
if (!await this.cloudPreCheckContinue(customer))
|
|
418
|
+
return null;
|
|
419
|
+
return await this.gate.run(() => this.doc.sync_call('<Limitr>.api.value', customer, entitlement));
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Get the cost for a standard increment on an entitlement (if set on its limit).
|
|
423
|
+
*/
|
|
424
|
+
async cost(id, entitlement) {
|
|
425
|
+
return await this.gate.run(() => this.doc.sync_call('<Limitr>.api.cost', id, entitlement));
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Try changing the value of a metered entitlement using a standard increment (defined in the Limit).
|
|
429
|
+
* This is the same as using "meter" with the "cost" of a standard increment for this entitlement.
|
|
430
|
+
* Returns true if changed and the limit was not hit, otherwise false and App.meter_limit lib func will be called (if present).
|
|
431
|
+
* Can use a string value for units in entitlement.limit.increment (must be a valid stof number) (ex. '3GiB' or '5s').
|
|
432
|
+
*/
|
|
433
|
+
async increment(customer, entitlement) {
|
|
434
|
+
if (!await this.cloudPreCheckContinue(customer))
|
|
435
|
+
return false;
|
|
436
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.increment', customer, entitlement));
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Try changing the value of a metered entitlement by removing a standard increment (defined in the Limit).
|
|
440
|
+
* This is the same as using "meter" with the negative "cost" of a standard increment for this entitlement.
|
|
441
|
+
* Can use a string value for units in entitlement.limit.increment (must be a valid stof number) (ex. '3GiB' or '5s').
|
|
442
|
+
*/
|
|
443
|
+
async deincrement(customer, entitlement) {
|
|
444
|
+
if (!await this.cloudPreCheckContinue(customer))
|
|
445
|
+
return false;
|
|
446
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.deincrement', customer, entitlement));
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Allow changing this entitlement by "value" for this customer?
|
|
450
|
+
* A boolean entitlement check if value is 0 or a limit does not exist for the entitlement (bool flag).
|
|
451
|
+
* Changes a meter for this customer if true.
|
|
452
|
+
* Can use a string value for units (must be a valid stof number) (ex. '3GiB' or '5s').
|
|
453
|
+
*/
|
|
454
|
+
async allow(customer, entitlement, value = 0) {
|
|
455
|
+
if (!await this.cloudPreCheckContinue(customer))
|
|
456
|
+
return false;
|
|
457
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.allow', customer, entitlement, value));
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Would a "check" call work for this entitlement and increment value on this customer?
|
|
461
|
+
* Does not change a meter (charge usage) for this customer, just checks if it would work.
|
|
462
|
+
*/
|
|
463
|
+
async checkIncrement(customer, entitlement) {
|
|
464
|
+
if (!await this.cloudPreCheckContinue(customer))
|
|
465
|
+
return false;
|
|
466
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.check_increment', customer, entitlement));
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Would a "check" call work for this entitlement and deincrement value on this customer?
|
|
470
|
+
* Does not change a meter (charge usage) for this customer, just checks if it would work.
|
|
471
|
+
*/
|
|
472
|
+
async checkDeincrement(customer, entitlement) {
|
|
473
|
+
if (!await this.cloudPreCheckContinue(customer))
|
|
474
|
+
return false;
|
|
475
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.check_deincrement', customer, entitlement));
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Would an "allow" call work for this entitlement and value on this customer?
|
|
479
|
+
* Does not change a meter (charge usage) for this customer, just checks if it would work.
|
|
480
|
+
* Can use a string value for units (must be a valid stof number) (ex. '3GiB' or '5s').
|
|
481
|
+
*/
|
|
482
|
+
async check(customer, entitlement, value = 0) {
|
|
483
|
+
if (!await this.cloudPreCheckContinue(customer))
|
|
484
|
+
return false;
|
|
485
|
+
return await this.gate.run(() => this.doc.call('<Limitr>.api.check', customer, entitlement, value));
|
|
486
|
+
}
|
|
487
|
+
/*****************************************************************************
|
|
488
|
+
* Limitr Cloud.
|
|
489
|
+
*****************************************************************************/
|
|
490
|
+
/**
|
|
491
|
+
* Initialize with cloud.limitr.dev.
|
|
492
|
+
*
|
|
493
|
+
* @param token API token.
|
|
494
|
+
* @param address (optional) Server URL.
|
|
495
|
+
* @param policy (optional) Policy ID or "active" for the active policy.
|
|
496
|
+
* @param connectTimeout (optional) Time to wait when establishing an initial connection (ms).
|
|
497
|
+
*/
|
|
498
|
+
static async cloud(options) {
|
|
499
|
+
const token = typeof options === 'string' ? options : options.token;
|
|
500
|
+
if (token.length < 1)
|
|
501
|
+
return undefined;
|
|
502
|
+
const policy = typeof options === 'string' ? 'active' : options.policy ?? 'active';
|
|
503
|
+
const address = typeof options === 'string' ? 'wss://api.limitr.dev' : options.wsAddress ?? 'wss://api.limitr.dev';
|
|
504
|
+
const ticketAddress = typeof options === 'string' ? 'https://api.limitr.dev' : options.ticketAddress ?? 'https://api.limitr.dev';
|
|
505
|
+
const timeout = typeof options === 'string' ? 5000 : options.connectTimeout ?? 5000;
|
|
506
|
+
const denyUnconnected = typeof options === 'string' ? true : options.denyUnconnected ?? true;
|
|
507
|
+
const response = await fetch(ticketAddress + '/wss/ticket', {
|
|
508
|
+
method: 'POST',
|
|
509
|
+
headers: { 'Content-Type': 'application/json' },
|
|
510
|
+
body: JSON.stringify({ token })
|
|
511
|
+
});
|
|
512
|
+
if (!response.ok)
|
|
513
|
+
return undefined;
|
|
514
|
+
const { ticket } = await response.json();
|
|
515
|
+
const ws = new WebSocket(address + '/wss?ticket=' + ticket);
|
|
516
|
+
ws.binaryType = 'arraybuffer';
|
|
517
|
+
const waitOpen = waitOnOpen(ws);
|
|
518
|
+
const limitr = await Limitr.new();
|
|
519
|
+
limitr.denyUnconnected = denyUnconnected;
|
|
520
|
+
const awaitInit = async () => {
|
|
521
|
+
await new Promise((resolve, reject) => {
|
|
522
|
+
const intervalMs = 50;
|
|
523
|
+
const start = Date.now();
|
|
524
|
+
const poll = () => {
|
|
525
|
+
if (limitr.wsInit) {
|
|
526
|
+
resolve(true);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (Date.now() - start > timeout) {
|
|
530
|
+
reject(new Error(`wait for WebSocket policy init timed out`));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
setTimeout(poll, intervalMs);
|
|
534
|
+
};
|
|
535
|
+
poll();
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
const reconnect = async () => {
|
|
539
|
+
const response = await fetch(ticketAddress + '/wss/ticket', {
|
|
540
|
+
method: 'POST',
|
|
541
|
+
headers: { 'Content-Type': 'application/json' },
|
|
542
|
+
body: JSON.stringify({ token })
|
|
543
|
+
});
|
|
544
|
+
if (response.ok) {
|
|
545
|
+
const { ticket } = await response.json();
|
|
546
|
+
const ws = new WebSocket(address + '/wss?ticket=' + ticket);
|
|
547
|
+
ws.binaryType = 'arraybuffer';
|
|
548
|
+
limitr.ws = ws;
|
|
549
|
+
limitr.ws.onclose = reconnect;
|
|
550
|
+
limitr.ws.onmessage = (m) => limitr.cloudMessageReceived(m);
|
|
551
|
+
await waitOnOpen(ws);
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
limitr.ws = ws;
|
|
555
|
+
limitr.ws.onclose = reconnect;
|
|
556
|
+
limitr.ws.onmessage = (m) => limitr.cloudMessageReceived(m);
|
|
557
|
+
await waitOpen;
|
|
558
|
+
limitr.ws.send(JSON.stringify({ type: 'policy', id: policy, format: 'bstf' }));
|
|
559
|
+
await awaitInit();
|
|
560
|
+
const ping = async () => {
|
|
561
|
+
if (!limitr.ws || limitr.ws.readyState === WebSocket.CLOSED || limitr.ws.readyState === WebSocket.CLOSING) {
|
|
562
|
+
await reconnect();
|
|
563
|
+
}
|
|
564
|
+
else if (limitr.ws.readyState === WebSocket.OPEN) {
|
|
565
|
+
if (limitr._dataSendQueue.length > 0) {
|
|
566
|
+
for (const data of limitr._dataSendQueue)
|
|
567
|
+
limitr.ws.send(data);
|
|
568
|
+
limitr._dataSendQueue = [];
|
|
569
|
+
}
|
|
570
|
+
limitr.ws.send('ping');
|
|
571
|
+
}
|
|
572
|
+
limitr.wsTimeout = setTimeout(ping, 20000);
|
|
573
|
+
};
|
|
574
|
+
limitr.wsTimeout = setTimeout(ping, 20000);
|
|
575
|
+
if (limitr._dataSendQueue.length > 0 && limitr.ws.readyState === WebSocket.OPEN) {
|
|
576
|
+
for (const data of limitr._dataSendQueue)
|
|
577
|
+
limitr.ws.send(data);
|
|
578
|
+
limitr._dataSendQueue = [];
|
|
579
|
+
}
|
|
580
|
+
return limitr;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Add a customer from the cloud.
|
|
584
|
+
* Default is to wait 3 seconds before moving on.
|
|
585
|
+
*/
|
|
586
|
+
async addCloudCustomer(id, timeout = 3000) {
|
|
587
|
+
const existing = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.customer', id));
|
|
588
|
+
if (existing || !this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
589
|
+
return false;
|
|
590
|
+
this._deniedCloudCustomers.delete(id);
|
|
591
|
+
this.ws.send(JSON.stringify({ type: 'customer', id }));
|
|
592
|
+
return new Promise((resolve) => {
|
|
593
|
+
const intervalMs = 50;
|
|
594
|
+
const start = Date.now();
|
|
595
|
+
const poll = async () => {
|
|
596
|
+
if (await this.gate.run(() => this.doc.sync_call('<Limitr>.api.customer', id))) {
|
|
597
|
+
resolve(true);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (this._deniedCloudCustomers.has(id)) {
|
|
601
|
+
this._deniedCloudCustomers.delete(id);
|
|
602
|
+
resolve(false);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (Date.now() - start > timeout) {
|
|
606
|
+
resolve(false);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
setTimeout(poll, intervalMs);
|
|
610
|
+
};
|
|
611
|
+
poll();
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Cloud pre-check.
|
|
616
|
+
*/
|
|
617
|
+
async cloudPreCheckContinue(customer) {
|
|
618
|
+
if (this.ws) {
|
|
619
|
+
switch (this.ws.readyState) {
|
|
620
|
+
case WebSocket.OPEN: {
|
|
621
|
+
const existing = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.customer', customer));
|
|
622
|
+
if (!existing)
|
|
623
|
+
return await this.addCloudCustomer(customer);
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
case WebSocket.CONNECTING: {
|
|
627
|
+
await waitOnOpen(this.ws);
|
|
628
|
+
const existing = await this.gate.run(() => this.doc.sync_call('<Limitr>.api.customer', customer));
|
|
629
|
+
if (!existing)
|
|
630
|
+
return await this.addCloudCustomer(customer);
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
case WebSocket.CLOSING:
|
|
634
|
+
case WebSocket.CLOSED: {
|
|
635
|
+
if (this.denyUnconnected)
|
|
636
|
+
return false;
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Close connection to cloud.limitr.dev.
|
|
645
|
+
*/
|
|
646
|
+
close() {
|
|
647
|
+
//@ts-ignore timeout
|
|
648
|
+
if (this.wsTimeout)
|
|
649
|
+
clearTimeout(this.wsTimeout);
|
|
650
|
+
this.wsInit = false;
|
|
651
|
+
if (this.ws && this.ws.readyState !== WebSocket.CLOSED && this.ws.readyState !== WebSocket.CLOSING) {
|
|
652
|
+
this.ws.onclose = () => { }; // clear any auto re-connect behavior
|
|
653
|
+
this.ws.close();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async wsSend(data) {
|
|
657
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
658
|
+
if (this._dataSendQueue.length > 0) {
|
|
659
|
+
for (const data of this._dataSendQueue)
|
|
660
|
+
this.ws.send(data);
|
|
661
|
+
this._dataSendQueue = [];
|
|
662
|
+
}
|
|
663
|
+
this.ws.send(data);
|
|
664
|
+
}
|
|
665
|
+
else if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
|
|
666
|
+
await waitOnOpen(this.ws);
|
|
667
|
+
if (this._dataSendQueue.length > 0) {
|
|
668
|
+
for (const data of this._dataSendQueue)
|
|
669
|
+
this.ws.send(data);
|
|
670
|
+
this._dataSendQueue = [];
|
|
671
|
+
}
|
|
672
|
+
this.ws.send(data);
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
this._dataSendQueue.push(data);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
//deno-lint-ignore no-explicit-any
|
|
679
|
+
async cloudMessageReceived(message) {
|
|
680
|
+
const data = message.data;
|
|
681
|
+
if (typeof data === 'string') {
|
|
682
|
+
if (data === 'pong' || data === 'ping')
|
|
683
|
+
return;
|
|
684
|
+
try {
|
|
685
|
+
const record = JSON.parse(data);
|
|
686
|
+
if (!!record.error && !!record.id) {
|
|
687
|
+
if (record.type === 'customer') {
|
|
688
|
+
this._deniedCloudCustomers.add(record.id);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
else if (!!record.policy && !!record.policy.plans) {
|
|
692
|
+
await this.gate.run(() => this.doc.sync_call('<Limitr>.api.update_policy_internals', data, 'json'));
|
|
693
|
+
}
|
|
694
|
+
else if (record.type === 'customer-invoices' && !!record.data.invoices && !!record.id) {
|
|
695
|
+
await this.gate.run(() => this.doc.sync_call('<Limitr>.api.update_customer_invoices', data, 'json'));
|
|
696
|
+
}
|
|
697
|
+
else if (!!record.type && !!record.id) {
|
|
698
|
+
await this.gate.run(() => this.doc.sync_call('<Limitr>.api.update_customer_internals', data, 'json'));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
// nada..
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
try {
|
|
707
|
+
let buffer;
|
|
708
|
+
if (data instanceof ArrayBuffer) {
|
|
709
|
+
buffer = new Uint8Array(data);
|
|
710
|
+
}
|
|
711
|
+
else if (data instanceof Blob) {
|
|
712
|
+
buffer = new Uint8Array(await data.arrayBuffer());
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
return; // unknown binary type
|
|
716
|
+
}
|
|
717
|
+
this.doc = new StofDoc();
|
|
718
|
+
this.doc.parse(buffer, 'bstf');
|
|
719
|
+
this.doc.lib('Http', 'fetch', async (url, method = 'GET', body = null, headers = new Map()) => {
|
|
720
|
+
const response = await fetch(url, {
|
|
721
|
+
method,
|
|
722
|
+
body: body ?? undefined,
|
|
723
|
+
headers: Object.fromEntries(headers.entries()),
|
|
724
|
+
});
|
|
725
|
+
const result = new Map();
|
|
726
|
+
result.set('status', response.status);
|
|
727
|
+
result.set('ok', response.ok);
|
|
728
|
+
const headerMap = new Map();
|
|
729
|
+
response.headers.forEach((value, key) => headerMap.set(key, value));
|
|
730
|
+
result.set('headers', headerMap);
|
|
731
|
+
result.set('content_type', response.headers.get('content-type') ?? response.headers.get('Content-Type') ?? 'text/plain');
|
|
732
|
+
result.set('bytes', await response.bytes());
|
|
733
|
+
return result;
|
|
734
|
+
}, true);
|
|
735
|
+
this.doc.lib('CloudWS', 'send', (data) => {
|
|
736
|
+
this.wsSend(data);
|
|
737
|
+
});
|
|
738
|
+
this.wsInit = true;
|
|
739
|
+
}
|
|
740
|
+
catch (e) {
|
|
741
|
+
console.error('Error initializing Limitr Policy from BSTF: ', e);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
//# sourceMappingURL=main.js.map
|