owostack 0.1.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/dist/index.js ADDED
@@ -0,0 +1,727 @@
1
+ // src/catalog.ts
2
+ var _featureRegistry = /* @__PURE__ */ new Map();
3
+ var FeatureMethods = class {
4
+ static async check(handle, customer, opts) {
5
+ if (!handle._client) {
6
+ throw new Error(
7
+ `Feature '${handle.slug}' is not bound to an Owostack client. Pass it inside a plan() in the catalog option of new Owostack({}).`
8
+ );
9
+ }
10
+ return handle._client.check({
11
+ customer,
12
+ feature: handle.slug,
13
+ ...opts
14
+ });
15
+ }
16
+ };
17
+ var BooleanHandle = class {
18
+ slug;
19
+ featureType = "boolean";
20
+ featureName;
21
+ _client = null;
22
+ constructor(slug, name) {
23
+ this.slug = slug;
24
+ this.featureName = name;
25
+ }
26
+ async check(customer, opts) {
27
+ return FeatureMethods.check(this, customer, opts);
28
+ }
29
+ on() {
30
+ return {
31
+ _type: "plan_feature",
32
+ slug: this.slug,
33
+ featureType: "boolean",
34
+ name: this.featureName,
35
+ enabled: true
36
+ };
37
+ }
38
+ off() {
39
+ return {
40
+ _type: "plan_feature",
41
+ slug: this.slug,
42
+ featureType: "boolean",
43
+ name: this.featureName,
44
+ enabled: false
45
+ };
46
+ }
47
+ };
48
+ function metered(slug, opts) {
49
+ const callable = (creditCost) => ({ feature: slug, creditCost });
50
+ const handleProps = {
51
+ slug,
52
+ featureType: "metered",
53
+ featureName: opts?.name,
54
+ _client: null,
55
+ async check(customer, checkOpts) {
56
+ return FeatureMethods.check(this, customer, checkOpts);
57
+ },
58
+ async track(customer, value = 1, trackOpts) {
59
+ const handle = this;
60
+ if (!handle._client) {
61
+ throw new Error(
62
+ `Feature '${slug}' is not bound to an Owostack client.`
63
+ );
64
+ }
65
+ return handle._client.track({
66
+ customer,
67
+ feature: slug,
68
+ value,
69
+ ...trackOpts
70
+ });
71
+ },
72
+ limit(value, config) {
73
+ return {
74
+ _type: "plan_feature",
75
+ slug,
76
+ featureType: "metered",
77
+ name: opts?.name,
78
+ enabled: true,
79
+ config: { limit: value, reset: "monthly", overage: "block", ...config }
80
+ };
81
+ },
82
+ included(value, config) {
83
+ return this.limit(value, config);
84
+ },
85
+ unlimited(config) {
86
+ return {
87
+ _type: "plan_feature",
88
+ slug,
89
+ featureType: "metered",
90
+ name: opts?.name,
91
+ enabled: true,
92
+ config: { limit: null, reset: "monthly", overage: "block", ...config }
93
+ };
94
+ },
95
+ config(configOpts) {
96
+ const isEnabled = configOpts.enabled !== false;
97
+ return {
98
+ _type: "plan_feature",
99
+ slug,
100
+ featureType: "metered",
101
+ name: opts?.name,
102
+ enabled: isEnabled,
103
+ config: { reset: "monthly", overage: "block", ...configOpts }
104
+ };
105
+ }
106
+ };
107
+ Object.assign(callable, handleProps);
108
+ _featureRegistry.set(slug, callable);
109
+ return callable;
110
+ }
111
+ function boolean(slug, opts) {
112
+ const handle = new BooleanHandle(slug, opts?.name);
113
+ _featureRegistry.set(slug, handle);
114
+ return handle;
115
+ }
116
+ var CreditSystemHandle = class {
117
+ slug;
118
+ name;
119
+ description;
120
+ featureCosts;
121
+ constructor(slug, featureCosts, opts) {
122
+ this.slug = slug;
123
+ this.featureCosts = featureCosts;
124
+ this.name = opts?.name;
125
+ this.description = opts?.description;
126
+ }
127
+ credits(amount, config) {
128
+ return {
129
+ _type: "plan_feature",
130
+ slug: this.slug,
131
+ featureType: "metered",
132
+ name: this.name,
133
+ enabled: true,
134
+ config: {
135
+ limit: amount,
136
+ reset: config?.reset || "monthly",
137
+ overage: config?.overage || "block"
138
+ }
139
+ };
140
+ }
141
+ _buildDefinition() {
142
+ return {
143
+ _type: "credit_system",
144
+ slug: this.slug,
145
+ name: this.name || this.slug,
146
+ description: this.description,
147
+ features: Array.from(this.featureCosts.entries()).map(
148
+ ([feature, creditCost]) => ({
149
+ feature,
150
+ creditCost
151
+ })
152
+ )
153
+ };
154
+ }
155
+ };
156
+ var _creditSystemRegistry = /* @__PURE__ */ new Map();
157
+ function creditSystem(slug, opts) {
158
+ const featureCosts = /* @__PURE__ */ new Map();
159
+ for (const featureEntry of opts.features) {
160
+ featureCosts.set(featureEntry.feature, featureEntry.creditCost);
161
+ }
162
+ const handle = new CreditSystemHandle(slug, featureCosts, {
163
+ name: opts.name,
164
+ description: opts.description
165
+ });
166
+ _creditSystemRegistry.set(slug, handle);
167
+ return handle;
168
+ }
169
+ function plan(slug, config) {
170
+ return {
171
+ _type: "plan",
172
+ slug,
173
+ ...config
174
+ };
175
+ }
176
+ function slugToName(slug) {
177
+ return slug.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
178
+ }
179
+ function buildSyncPayload(catalog, defaultProvider) {
180
+ const featureMap = /* @__PURE__ */ new Map();
181
+ for (const entry of catalog) {
182
+ if (entry._type !== "plan") continue;
183
+ for (const f of entry.features) {
184
+ if (_creditSystemRegistry.has(f.slug)) continue;
185
+ if (!featureMap.has(f.slug)) {
186
+ featureMap.set(f.slug, {
187
+ slug: f.slug,
188
+ type: f.featureType,
189
+ name: f.name || slugToName(f.slug)
190
+ });
191
+ }
192
+ }
193
+ }
194
+ const creditSystems = catalog.filter((e) => e._type === "credit_system").map((cs) => ({
195
+ slug: cs.slug,
196
+ name: cs.name,
197
+ description: cs.description,
198
+ features: cs.features
199
+ }));
200
+ for (const entry of catalog) {
201
+ if (entry._type !== "plan") continue;
202
+ for (const f of entry.features) {
203
+ const csHandle = _creditSystemRegistry.get(f.slug);
204
+ if (csHandle && !creditSystems.find((cs) => cs.slug === f.slug)) {
205
+ const def = csHandle._buildDefinition();
206
+ creditSystems.push({
207
+ slug: def.slug,
208
+ name: def.name,
209
+ description: def.description,
210
+ features: def.features
211
+ });
212
+ }
213
+ }
214
+ }
215
+ const plans = catalog.filter((e) => e._type === "plan").map((p) => ({
216
+ slug: p.slug,
217
+ name: p.name,
218
+ description: p.description ?? void 0,
219
+ price: p.price,
220
+ currency: p.currency,
221
+ interval: p.interval,
222
+ planGroup: p.planGroup ?? void 0,
223
+ trialDays: p.trialDays ?? void 0,
224
+ provider: p.provider ?? void 0,
225
+ metadata: p.metadata ?? void 0,
226
+ features: p.features.map((f) => ({
227
+ slug: f.slug,
228
+ enabled: f.enabled,
229
+ // Boolean features have no limit concept (null), metered features use limit from config
230
+ limit: f.featureType === "boolean" ? null : f.config?.limit ?? null,
231
+ // Boolean features have no reset interval
232
+ ...f.featureType !== "boolean" && {
233
+ reset: f.config?.reset || "monthly"
234
+ },
235
+ ...f.config?.overage && { overage: f.config.overage },
236
+ ...f.config?.overagePrice !== void 0 && {
237
+ overagePrice: f.config.overagePrice
238
+ },
239
+ ...f.config?.maxOverageUnits !== void 0 && {
240
+ maxOverageUnits: f.config.maxOverageUnits
241
+ },
242
+ ...f.config?.billingUnits !== void 0 && {
243
+ billingUnits: f.config.billingUnits
244
+ },
245
+ ...f.config?.creditCost !== void 0 && {
246
+ creditCost: f.config.creditCost
247
+ }
248
+ }))
249
+ }));
250
+ return {
251
+ defaultProvider,
252
+ features: Array.from(featureMap.values()),
253
+ creditSystems,
254
+ plans
255
+ };
256
+ }
257
+ function bindFeatureHandles(client, catalog) {
258
+ if (catalog) {
259
+ const slugsInCatalog = /* @__PURE__ */ new Set();
260
+ for (const entry of catalog) {
261
+ if (entry._type === "plan") {
262
+ for (const f of entry.features) {
263
+ slugsInCatalog.add(f.slug);
264
+ }
265
+ }
266
+ }
267
+ for (const [slug, handle] of _featureRegistry) {
268
+ if (slugsInCatalog.has(slug)) {
269
+ handle._client = client;
270
+ }
271
+ }
272
+ } else {
273
+ for (const handle of _featureRegistry.values()) {
274
+ handle._client = client;
275
+ }
276
+ }
277
+ }
278
+
279
+ // src/index.ts
280
+ var Owostack = class {
281
+ /** @internal — exposed for CLI tooling */
282
+ _config;
283
+ apiUrl;
284
+ /** Billing: unbilled usage, invoices, and invoice generation */
285
+ billing;
286
+ /** Wallet: payment methods — callable + namespace */
287
+ wallet;
288
+ constructor(config) {
289
+ this._config = config;
290
+ this.apiUrl = this.resolveApiUrl(config);
291
+ this.billing = new BillingNamespace(this);
292
+ this.wallet = buildWalletFn(this);
293
+ this.plans = buildPlansFn(this);
294
+ if (config.catalog && config.catalog.length > 0) {
295
+ bindFeatureHandles(this, config.catalog);
296
+ }
297
+ }
298
+ resolveApiUrl(config) {
299
+ if (config.apiUrl) {
300
+ return config.apiUrl;
301
+ }
302
+ if (config.mode === "sandbox") {
303
+ return "https://sandbox.owostack.com";
304
+ }
305
+ if (config.mode === "live") {
306
+ return "https://api.owostack.com";
307
+ }
308
+ return "https://api.owostack.com";
309
+ }
310
+ /**
311
+ * Override the API key at runtime (used by CLI).
312
+ * @internal
313
+ */
314
+ setSecretKey(key) {
315
+ this._config.secretKey = key;
316
+ }
317
+ /**
318
+ * Override the API URL at runtime (used by CLI).
319
+ * @internal
320
+ */
321
+ setApiUrl(url) {
322
+ this._config.apiUrl = url;
323
+ this.apiUrl = url;
324
+ }
325
+ /**
326
+ * sync() - Push catalog to the API
327
+ *
328
+ * Reconciles features and plans defined in the catalog with the server.
329
+ * Idempotent — safe to call multiple times with the same config.
330
+ * Only creates/updates; never deletes.
331
+ *
332
+ * @example
333
+ * ```ts
334
+ * const result = await owo.sync();
335
+ * console.log(`Created ${result.features.created.length} features`);
336
+ * ```
337
+ */
338
+ async sync() {
339
+ if (!this._config.catalog || this._config.catalog.length === 0) {
340
+ return {
341
+ success: true,
342
+ features: { created: [], updated: [], unchanged: [] },
343
+ creditSystems: { created: [], updated: [], unchanged: [] },
344
+ plans: { created: [], updated: [], unchanged: [] },
345
+ warnings: ["No catalog entries to sync."]
346
+ };
347
+ }
348
+ const payload = buildSyncPayload(
349
+ this._config.catalog,
350
+ this._config.provider
351
+ );
352
+ const response = await this.post("/sync", payload);
353
+ return response;
354
+ }
355
+ /**
356
+ * attach() - Checkout & Subscription Management
357
+ *
358
+ * Creates checkout sessions, handles upgrades/downgrades, manages subscriptions.
359
+ * Auto-creates the customer if customerData is provided and the customer doesn't exist.
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * const result = await owo.attach({
364
+ * customer: 'user_123',
365
+ * product: 'pro_plan',
366
+ * customerData: { email: 'user@example.com', name: 'Jane' },
367
+ * });
368
+ *
369
+ * // Redirect user to checkout
370
+ * window.location.href = result.url;
371
+ * ```
372
+ */
373
+ async attach(params) {
374
+ const response = await this.post("/attach", params);
375
+ return response;
376
+ }
377
+ /**
378
+ * check() - Feature Gating & Access Control
379
+ *
380
+ * Queries whether a customer can access a feature based on their plan,
381
+ * payment status, and usage limits.
382
+ *
383
+ * Pass sendEvent: true to atomically check AND track usage in one call.
384
+ *
385
+ * @example
386
+ * ```ts
387
+ * const access = await owo.check({
388
+ * customer: 'user_123',
389
+ * feature: 'api_calls',
390
+ * customerData: { email: 'user@example.com' },
391
+ * });
392
+ *
393
+ * if (!access.allowed) {
394
+ * throw new Error(access.code);
395
+ * }
396
+ * ```
397
+ */
398
+ async check(params) {
399
+ const response = await this.post("/check", params);
400
+ return response;
401
+ }
402
+ /**
403
+ * track() - Usage Metering & Billing
404
+ *
405
+ * Records usage events, decrements quotas, and triggers billing for
406
+ * pay-as-you-go features.
407
+ *
408
+ * @example
409
+ * ```ts
410
+ * await owo.track({
411
+ * customer: 'user_123',
412
+ * feature: 'api_calls',
413
+ * value: 1,
414
+ * customerData: { email: 'user@example.com' },
415
+ * });
416
+ * ```
417
+ */
418
+ async track(params) {
419
+ const response = await this.post("/track", params);
420
+ return response;
421
+ }
422
+ /**
423
+ * addon() - Purchase Add-on Credit Pack
424
+ *
425
+ * Buy additional credits scoped to a credit system.
426
+ * If the customer has a card on file, charges immediately.
427
+ * Otherwise returns a checkout URL.
428
+ *
429
+ * @example
430
+ * ```ts
431
+ * const result = await owo.addon({
432
+ * customer: 'user_123',
433
+ * pack: '250-credits',
434
+ * quantity: 2,
435
+ * });
436
+ *
437
+ * if (result.requiresCheckout) {
438
+ * window.location.href = result.checkoutUrl!;
439
+ * } else {
440
+ * console.log(`Balance: ${result.balance} credits`);
441
+ * }
442
+ * ```
443
+ */
444
+ async addon(params) {
445
+ const response = await this.post("/addon", params);
446
+ return response;
447
+ }
448
+ /**
449
+ * customer() - Create or resolve a customer
450
+ *
451
+ * Creates a new customer or resolves an existing one by email or ID.
452
+ * If customerData is provided and customer doesn't exist, creates a new customer.
453
+ *
454
+ * @example
455
+ * ```ts
456
+ * // Create new customer
457
+ * const customer = await owo.customer({
458
+ * email: 'org@acme.com',
459
+ * name: 'Acme Corp',
460
+ * metadata: { plan: 'enterprise' }
461
+ * });
462
+ *
463
+ * // Get existing customer
464
+ * const existing = await owo.customer({ email: 'org@acme.com' });
465
+ * ```
466
+ */
467
+ async customer(params) {
468
+ const response = await this.post("/customers", params);
469
+ return response;
470
+ }
471
+ /**
472
+ * addEntity() - Add a feature entity (e.g., seat)
473
+ *
474
+ * Creates a new entity scoped to a feature. Validates against the feature limit.
475
+ * Throws if the limit would be exceeded.
476
+ *
477
+ * @example
478
+ * ```ts
479
+ * await owo.addEntity({
480
+ * customer: 'org@acme.com',
481
+ * feature: 'seats',
482
+ * entity: 'user_123',
483
+ * name: 'John Doe',
484
+ * metadata: { role: 'admin' }
485
+ * });
486
+ * ```
487
+ */
488
+ async addEntity(params) {
489
+ const response = await this.post("/entities", params);
490
+ return response;
491
+ }
492
+ /**
493
+ * removeEntity() - Remove a feature entity
494
+ *
495
+ * Removes an entity and frees up the slot for the feature limit.
496
+ *
497
+ * @example
498
+ * ```ts
499
+ * await owo.removeEntity({
500
+ * customer: 'org@acme.com',
501
+ * feature: 'seats',
502
+ * entity: 'user_123'
503
+ * });
504
+ * ```
505
+ */
506
+ async removeEntity(params) {
507
+ const response = await this.post("/entities/remove", params);
508
+ return response;
509
+ }
510
+ /**
511
+ * listEntities() - List feature entities
512
+ *
513
+ * Returns all entities for a customer, optionally filtered by feature.
514
+ *
515
+ * @example
516
+ * ```ts
517
+ * // List all entities
518
+ * const { entities } = await owo.listEntities({ customer: 'org@acme.com' });
519
+ *
520
+ * // List seats only
521
+ * const { entities: seats } = await owo.listEntities({
522
+ * customer: 'org@acme.com',
523
+ * feature: 'seats'
524
+ * });
525
+ * ```
526
+ */
527
+ async listEntities(params) {
528
+ const query = { customer: params.customer };
529
+ if (params.feature) query.feature = params.feature;
530
+ const response = await this.get("/entities", query);
531
+ return response;
532
+ }
533
+ /**
534
+ * plans() - List Plans
535
+ *
536
+ * Returns all active plans for the organization. Useful for building
537
+ * pricing pages and plan selection UIs.
538
+ *
539
+ * @example
540
+ * ```ts
541
+ * const { plans } = await owo.plans();
542
+ * plans.forEach(p => console.log(p.name, p.price));
543
+ *
544
+ * // Filter by group
545
+ * const { plans: support } = await owo.plans({ group: 'support' });
546
+ *
547
+ * // Get a single plan by slug
548
+ * const plan = await owo.plans.get('pro');
549
+ * ```
550
+ */
551
+ plans;
552
+ /**
553
+ * Internal POST request handler
554
+ * @internal
555
+ */
556
+ async post(endpoint, body) {
557
+ const response = await fetch(`${this.apiUrl}${endpoint}`, {
558
+ method: "POST",
559
+ headers: {
560
+ "Content-Type": "application/json",
561
+ Authorization: `Bearer ${this._config.secretKey}`
562
+ },
563
+ body: JSON.stringify(body)
564
+ });
565
+ if (!response.ok) {
566
+ const resp = await response.json();
567
+ const errorData = resp.error || resp;
568
+ throw new OwostackError(
569
+ errorData.code || "unknown_error",
570
+ errorData.message || errorData.error || "Request failed"
571
+ );
572
+ }
573
+ return response.json();
574
+ }
575
+ /**
576
+ * Internal GET request handler
577
+ * @internal
578
+ */
579
+ async get(endpoint, query) {
580
+ const url = new URL(`${this.apiUrl}${endpoint}`);
581
+ if (query) {
582
+ for (const [key, value] of Object.entries(query)) {
583
+ if (value !== void 0 && value !== "")
584
+ url.searchParams.set(key, value);
585
+ }
586
+ }
587
+ const response = await fetch(url.toString(), {
588
+ method: "GET",
589
+ headers: {
590
+ Authorization: `Bearer ${this._config.secretKey}`
591
+ }
592
+ });
593
+ if (!response.ok) {
594
+ const resp = await response.json();
595
+ const errorData = resp.error || resp;
596
+ throw new OwostackError(
597
+ errorData.code || "unknown_error",
598
+ errorData.message || errorData.error || "Request failed"
599
+ );
600
+ }
601
+ return response.json();
602
+ }
603
+ };
604
+ function buildPlansFn(client) {
605
+ const fn = ((params) => {
606
+ const query = {};
607
+ if (params?.group) query.group = params.group;
608
+ if (params?.interval) query.interval = params.interval;
609
+ if (params?.currency) query.currency = params.currency;
610
+ if (params?.includeInactive) query.includeInactive = "true";
611
+ return client.get("/plans", query);
612
+ });
613
+ fn.get = async (slug) => {
614
+ const response = await client.get(`/plans/${encodeURIComponent(slug)}`);
615
+ return response.plan;
616
+ };
617
+ return fn;
618
+ }
619
+ function buildWalletFn(client) {
620
+ const fn = ((customer) => client.get("/wallet", { customer }));
621
+ fn.setup = (customer, opts) => client.post("/wallet/setup", {
622
+ customer,
623
+ ...opts
624
+ });
625
+ fn.list = (customer) => client.get("/wallet", { customer });
626
+ fn.remove = (customer, id) => client.post("/wallet/remove", {
627
+ customer,
628
+ id
629
+ });
630
+ return fn;
631
+ }
632
+ var BillingNamespace = class {
633
+ constructor(client) {
634
+ this.client = client;
635
+ }
636
+ /**
637
+ * usage() - Get Unbilled Overage Usage
638
+ *
639
+ * Returns a breakdown of all billable usage that hasn't been invoiced yet.
640
+ *
641
+ * @example
642
+ * ```ts
643
+ * const usage = await owo.billing.usage({ customer: 'user_123' });
644
+ * console.log(`Owes: ${usage.currency} ${usage.totalEstimated / 100}`);
645
+ * ```
646
+ */
647
+ async usage(params) {
648
+ const response = await this.client.get("/billing/usage", {
649
+ customer: params.customer
650
+ });
651
+ return response;
652
+ }
653
+ /**
654
+ * invoice() - Generate an Invoice
655
+ *
656
+ * Creates an invoice for the customer's unbilled overage usage.
657
+ * Fails if there's no unbilled usage to invoice.
658
+ *
659
+ * @example
660
+ * ```ts
661
+ * const result = await owo.billing.invoice({ customer: 'user_123' });
662
+ * console.log(`Invoice ${result.invoice.number}: ${result.invoice.total}`);
663
+ * ```
664
+ */
665
+ async invoice(params) {
666
+ const response = await this.client.post("/billing/invoice", params);
667
+ return response;
668
+ }
669
+ /**
670
+ * invoices() - List Past Invoices
671
+ *
672
+ * Returns all invoices for a customer.
673
+ *
674
+ * @example
675
+ * ```ts
676
+ * const result = await owo.billing.invoices({ customer: 'user_123' });
677
+ * result.invoices.forEach(inv => console.log(inv.number, inv.status));
678
+ * ```
679
+ */
680
+ async invoices(params) {
681
+ const response = await this.client.get("/billing/invoices", {
682
+ customer: params.customer
683
+ });
684
+ return response;
685
+ }
686
+ /**
687
+ * pay() - Pay an Invoice
688
+ *
689
+ * Attempts to auto-charge the customer's saved payment method.
690
+ * If no card on file or charge fails, returns a checkout URL instead.
691
+ *
692
+ * @example
693
+ * ```ts
694
+ * const result = await owo.billing.pay({ invoiceId: 'inv_xxx' });
695
+ * if (!result.paid) {
696
+ * // Redirect customer to checkout
697
+ * window.location.href = result.checkoutUrl!;
698
+ * }
699
+ * ```
700
+ */
701
+ async pay(params) {
702
+ const response = await this.client.post(
703
+ `/billing/invoice/${encodeURIComponent(params.invoiceId)}/pay`,
704
+ { callbackUrl: params.callbackUrl }
705
+ );
706
+ return response;
707
+ }
708
+ };
709
+ var OwostackError = class extends Error {
710
+ code;
711
+ constructor(code, message) {
712
+ super(message);
713
+ this.name = "OwostackError";
714
+ this.code = code;
715
+ }
716
+ };
717
+ export {
718
+ BooleanHandle,
719
+ CreditSystemHandle,
720
+ Owostack,
721
+ OwostackError,
722
+ boolean,
723
+ buildSyncPayload,
724
+ creditSystem,
725
+ metered,
726
+ plan
727
+ };