@develemit/billing 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/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @develemit/billing
2
+
3
+ Typed, fetch-based client for the emit-billing API. Server-side only — the
4
+ API key is a project-level secret, and entitlement checks must never happen
5
+ from a browser with that key exposed.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @develemit/billing
11
+ ```
12
+
13
+ ## Afternoon integration
14
+
15
+ ```ts
16
+ import { createClient, BillingApiError } from '@develemit/billing';
17
+
18
+ const billing = createClient({
19
+ apiKey: process.env.BILLING_API_KEY!,
20
+ baseUrl: 'https://billing.example.com',
21
+ });
22
+
23
+ // Gate a route
24
+ app.get('/reports/export', async (req, res) => {
25
+ const entitlements = await billing.getEntitlements(req.subjectId);
26
+ if (!entitlements.features.export) {
27
+ return res.status(403).json({ error: 'upgrade_required' });
28
+ }
29
+ // entitlements.stale === true means this came from cache after an API
30
+ // outage — still safe to trust for gating, just not guaranteed fresh.
31
+ return res.json(await buildExportReport());
32
+ });
33
+
34
+ // Start checkout
35
+ app.post('/upgrade', async (req, res) => {
36
+ try {
37
+ const { url } = await billing.createCheckout({
38
+ planKey: 'pro',
39
+ subjectId: req.subjectId,
40
+ successUrl: 'https://app.example.com/billing/success',
41
+ cancelUrl: 'https://app.example.com/billing',
42
+ });
43
+ return res.redirect(url);
44
+ } catch (err) {
45
+ if (err instanceof BillingApiError) {
46
+ return res.status(502).json({ error: err.code });
47
+ }
48
+ throw err;
49
+ }
50
+ });
51
+ ```
52
+
53
+ ## Route gating (Fastify)
54
+
55
+ `billingGuardPlugin` wraps `getEntitlements` with a fail-open/fail-closed
56
+ policy for route guards, so every gated route states — explicitly, at the
57
+ type level — what happens when billing itself is unreachable.
58
+
59
+ It lives at the `@develemit/billing/fastify` subpath rather than the
60
+ package root, so importing the core client never pulls in Fastify's types —
61
+ install `fastify` yourself (it's a peer dependency) only if you use this
62
+ part of the SDK.
63
+
64
+ ```ts
65
+ import { createClient } from '@develemit/billing';
66
+ import { billingGuardPlugin } from '@develemit/billing/fastify';
67
+
68
+ const billing = createClient({
69
+ apiKey: process.env.BILLING_API_KEY!,
70
+ baseUrl: 'https://billing.example.com',
71
+ });
72
+
73
+ await app.register(billingGuardPlugin, {
74
+ client: billing,
75
+ getSubjectId: (req) => req.household.id, // your app's Subject mapping
76
+ onUnavailable: 'closed', // the plugin-wide default; routes may override
77
+ });
78
+
79
+ // Read feature: an outage shouldn't block reads, so fail open.
80
+ app.get('/reports/export', {
81
+ preHandler: [app.requireEntitlement('export', { onUnavailable: 'open' })],
82
+ handler: async (req) => buildExportReport(req.entitlements!),
83
+ });
84
+
85
+ // Payments-adjacent write: an outage means we can't confirm entitlement,
86
+ // so fail closed rather than risk letting an unpaid write through.
87
+ app.post('/invoices/send', {
88
+ preHandler: [
89
+ app.requireEntitlement('send_invoices', { onUnavailable: 'closed' }),
90
+ ],
91
+ handler: async (req) => sendInvoice(req.body),
92
+ });
93
+
94
+ // Limit check: the consumer app owns metering (billing only compares).
95
+ app.post('/emails/send', {
96
+ preHandler: [
97
+ app.requireWithinLimit(
98
+ 'emails_per_month',
99
+ (req) => req.org.emailsSentThisMonth,
100
+ ),
101
+ ],
102
+ handler: async (req) => sendEmail(req.body),
103
+ });
104
+ ```
105
+
106
+ Decision outcomes:
107
+
108
+ - Entitlements resolve (fresh or stale) → decided on content: feature
109
+ truthy, or usage under the limit → allow (`request.entitlements`
110
+ decorated); otherwise `403 entitlement_denied`.
111
+ - Billing unreachable (network failure, or no cached value to fall back
112
+ on) → `onUnavailable: 'open'` allows the request through; `'closed'`
113
+ returns `503 billing_unavailable`.
114
+ - A 4xx from the API (bad key, malformed subject) is misconfiguration, not
115
+ an outage — always denied (`403 billing_misconfigured`) regardless of
116
+ `onUnavailable`, so fail-open can't mask a broken integration.
117
+
118
+ Choosing a policy: `'closed'` for anything payments-adjacent or otherwise
119
+ risky to let through unchecked; `'open'` for ordinary reads where a
120
+ billing outage shouldn't take down an unrelated feature.
121
+
122
+ The framework-agnostic decision function (`checkEntitlement` from
123
+ `./guard.js`) is exported separately for building adapters to other
124
+ frameworks.
125
+
126
+ ## Behavior
127
+
128
+ - `getEntitlements(subjectId)` caches per-subject for `entitlementsTtlMs`
129
+ (default 5000ms). Within the TTL it never hits the network. Past the TTL
130
+ it refetches; if the API is unreachable or returns a 5xx and a cached
131
+ value exists, it returns that value with `stale: true` instead of
132
+ throwing. It only throws when there's no cached value, or on a 4xx (bad
133
+ key, validation) — staleness never masks misconfiguration.
134
+ - `createCheckout`, `portalUrl`, and `getSubscription` always call the API
135
+ and throw `BillingApiError` on any failure.
136
+ - Every failure is a `BillingApiError` with `status` and `code`. Network
137
+ failures use `status: 0, code: 'network_error'` so callers can tell
138
+ "billing is unreachable" apart from "request was denied".
@@ -0,0 +1,495 @@
1
+ // ../shared-types/src/admin.ts
2
+ import { z as z5 } from "zod";
3
+
4
+ // ../shared-types/src/event-log.ts
5
+ import { z } from "zod";
6
+ var eventStatusSchema = z.enum([
7
+ "received",
8
+ "processed",
9
+ "failed",
10
+ "dead_letter",
11
+ "discarded"
12
+ ]);
13
+ var subscriptionStateSchema = z.enum([
14
+ "trialing",
15
+ "active",
16
+ "past_due",
17
+ "canceled",
18
+ "unpaid"
19
+ ]);
20
+ var adminEventSummarySchema = z.object({
21
+ id: z.string(),
22
+ projectId: z.string(),
23
+ providerEventId: z.string(),
24
+ type: z.string(),
25
+ status: eventStatusSchema,
26
+ attempts: z.number(),
27
+ lastError: z.string().nullable(),
28
+ errorSignature: z.string().nullable(),
29
+ receivedAt: z.iso.datetime(),
30
+ processedAt: z.iso.datetime().nullable(),
31
+ // event_log has no subject/customer reference column (sprint 24's
32
+ // repo-drizzle-subjects.ts documents the same gap the other direction) —
33
+ // derived from the raw Stripe payload's well-known `data.object.customer`
34
+ // path, so this is null for any non-Stripe-shaped payload.
35
+ subjectRef: z.string().nullable()
36
+ });
37
+ var adminEventDetailSchema = adminEventSummarySchema.extend({
38
+ payload: z.record(z.string(), z.unknown()),
39
+ discardReason: z.string().nullable(),
40
+ discardedBy: z.string().nullable(),
41
+ discardedAt: z.iso.datetime().nullable()
42
+ });
43
+ var signatureCountSchema = z.object({
44
+ signature: z.string(),
45
+ count: z.number()
46
+ });
47
+ var listEventsResponseSchema = z.object({
48
+ events: z.array(adminEventSummarySchema),
49
+ total: z.number(),
50
+ limit: z.number(),
51
+ offset: z.number(),
52
+ signatureCounts: z.array(signatureCountSchema)
53
+ });
54
+ var wouldApplySchema = z.discriminatedUnion("kind", [
55
+ z.object({ kind: z.literal("create"), to: subscriptionStateSchema }),
56
+ z.object({
57
+ kind: z.literal("transition"),
58
+ from: subscriptionStateSchema,
59
+ to: subscriptionStateSchema
60
+ }),
61
+ z.object({ kind: z.literal("stale") }),
62
+ z.object({
63
+ kind: z.literal("invalid"),
64
+ from: subscriptionStateSchema,
65
+ to: subscriptionStateSchema
66
+ }),
67
+ z.object({ kind: z.literal("unknown_subscription") }),
68
+ z.object({ kind: z.literal("noop"), reason: z.string() }),
69
+ z.object({ kind: z.literal("error"), reason: z.string() })
70
+ ]);
71
+ var dryRunReplayResponseSchema = z.object({
72
+ event: adminEventDetailSchema,
73
+ current: z.object({
74
+ state: subscriptionStateSchema,
75
+ lastEventAt: z.iso.datetime().nullable()
76
+ }).nullable(),
77
+ wouldApply: wouldApplySchema
78
+ });
79
+ var replayOutcomeSchema = z.enum([
80
+ "created",
81
+ "transitioned",
82
+ "stale",
83
+ "noop",
84
+ "invalid",
85
+ "unknown_subscription",
86
+ "already_processed",
87
+ "error"
88
+ ]);
89
+ var replayResponseSchema = z.object({
90
+ outcome: replayOutcomeSchema
91
+ });
92
+ var discardEventRequestSchema = z.object({
93
+ actor: z.string().min(1),
94
+ reason: z.string()
95
+ });
96
+ var replayGroupCursorSchema = z.object({
97
+ receivedAt: z.iso.datetime(),
98
+ id: z.string()
99
+ });
100
+ var replayGroupRequestSchema = z.object({
101
+ projectId: z.string().min(1),
102
+ cursor: replayGroupCursorSchema.optional(),
103
+ limit: z.number().int().positive().optional()
104
+ }).and(
105
+ z.union([
106
+ z.object({ errorSignature: z.string().min(1) }),
107
+ z.object({ all: z.literal(true) })
108
+ ])
109
+ );
110
+ var replayGroupOutcomeEntrySchema = z.object({
111
+ eventId: z.string(),
112
+ outcome: replayOutcomeSchema
113
+ });
114
+ var replayGroupResponseSchema = z.object({
115
+ outcomes: z.array(replayGroupOutcomeEntrySchema),
116
+ // Keyed by ProcessOutcome, but zod v4's record needs .partial() semantics
117
+ // for an exhaustive enum key type, so this stays a plain string map — the
118
+ // set of possible keys is still exactly replayOutcomeSchema's members.
119
+ counts: z.record(z.string(), z.number()),
120
+ // This call's batch (its own limit, or BULK_REPLAY_CAP by default) hit its
121
+ // ceiling: more matching events remain beyond what was just replayed.
122
+ // Paging with nextCursor continues; a non-paging caller reads this the
123
+ // same way it always has, as "replay again to finish draining it."
124
+ truncated: z.boolean(),
125
+ nextCursor: replayGroupCursorSchema.nullable()
126
+ });
127
+
128
+ // ../shared-types/src/entitlements.ts
129
+ import { z as z3 } from "zod";
130
+
131
+ // ../shared-types/src/subscriptions.ts
132
+ import { z as z2 } from "zod";
133
+ var subscriptionStatusSchema = z2.enum([
134
+ "trialing",
135
+ "active",
136
+ "past_due",
137
+ "grace",
138
+ "canceled",
139
+ "unpaid",
140
+ "none"
141
+ ]);
142
+ var activeSubscriptionViewSchema = z2.object({
143
+ state: subscriptionStatusSchema.exclude(["none"]),
144
+ planKey: z2.string(),
145
+ currentPeriodEnd: z2.iso.datetime(),
146
+ cancelAtPeriodEnd: z2.boolean()
147
+ });
148
+ var noSubscriptionViewSchema = z2.object({
149
+ state: z2.literal("none")
150
+ });
151
+ var subscriptionViewSchema = z2.discriminatedUnion("state", [
152
+ activeSubscriptionViewSchema,
153
+ noSubscriptionViewSchema
154
+ ]);
155
+
156
+ // ../shared-types/src/entitlements.ts
157
+ var entitlementSourcesSchema = z3.object({
158
+ features: z3.record(z3.string(), z3.enum(["subscription", "override"])),
159
+ limits: z3.record(z3.string(), z3.enum(["subscription", "override"]))
160
+ });
161
+ var entitlementsResponseSchema = z3.object({
162
+ plan: z3.string().nullable(),
163
+ features: z3.record(z3.string(), z3.boolean()),
164
+ limits: z3.record(z3.string(), z3.number()),
165
+ status: subscriptionStatusSchema,
166
+ source: z3.enum(["subscription", "override", "none"]),
167
+ sources: entitlementSourcesSchema
168
+ });
169
+
170
+ // ../shared-types/src/overrides.ts
171
+ import { z as z4 } from "zod";
172
+ var overrideKindSchema = z4.enum(["comp", "extend", "revoke"]);
173
+ var overrideTargetSchema = z4.discriminatedUnion("type", [
174
+ z4.object({ type: z4.literal("feature"), feature: z4.string().min(1) }),
175
+ z4.object({
176
+ type: z4.literal("limit"),
177
+ limit: z4.string().min(1),
178
+ value: z4.number()
179
+ })
180
+ ]);
181
+ var createOverrideRequestSchema = z4.object({
182
+ subjectId: z4.string().min(1),
183
+ kind: overrideKindSchema,
184
+ target: overrideTargetSchema,
185
+ actor: z4.string().min(1),
186
+ reason: z4.string().min(4),
187
+ expiresAt: z4.iso.datetime().optional()
188
+ });
189
+ var createBulkOverrideRequestSchema = z4.object({
190
+ subjectId: z4.string().min(1),
191
+ kind: overrideKindSchema,
192
+ targets: z4.array(overrideTargetSchema).min(1),
193
+ actor: z4.string().min(1),
194
+ reason: z4.string().min(4),
195
+ expiresAt: z4.iso.datetime().optional()
196
+ });
197
+ var overrideRecordSchema = z4.object({
198
+ id: z4.string(),
199
+ subjectId: z4.string(),
200
+ kind: overrideKindSchema,
201
+ target: overrideTargetSchema,
202
+ actor: z4.string(),
203
+ reason: z4.string(),
204
+ expiresAt: z4.iso.datetime().nullable(),
205
+ createdAt: z4.iso.datetime()
206
+ });
207
+ var bulkOverrideResponseSchema = z4.object({
208
+ overrides: z4.array(overrideRecordSchema)
209
+ });
210
+ var listOverridesResponseSchema = z4.object({
211
+ overrides: z4.array(overrideRecordSchema),
212
+ total: z4.number(),
213
+ limit: z4.number(),
214
+ offset: z4.number()
215
+ });
216
+
217
+ // ../shared-types/src/admin.ts
218
+ var stateCountsSchema = z5.object({
219
+ trialing: z5.number(),
220
+ active: z5.number(),
221
+ past_due: z5.number(),
222
+ grace: z5.number(),
223
+ canceled: z5.number(),
224
+ unpaid: z5.number()
225
+ });
226
+ var dashboardMetricsSchema = z5.object({
227
+ mrrCents: z5.number(),
228
+ mrrPrevCents: z5.number(),
229
+ subjectsCount: z5.number(),
230
+ states: stateCountsSchema,
231
+ deadLetterDepth: z5.number(),
232
+ oldestUnprocessedSec: z5.number().nullable(),
233
+ driftCount: z5.number()
234
+ });
235
+ var projectMetricsSchema = dashboardMetricsSchema.extend({
236
+ projectId: z5.string(),
237
+ projectKey: z5.string(),
238
+ projectName: z5.string(),
239
+ subjectType: z5.string(),
240
+ environment: z5.enum(["test", "live"]),
241
+ envLocked: z5.boolean(),
242
+ mrrSeries: z5.array(z5.number())
243
+ });
244
+ var adminMetricsResponseSchema = z5.object({
245
+ ecosystem: dashboardMetricsSchema,
246
+ // Shared x-axis labels for every project's mrrSeries (same calendar
247
+ // months across the ecosystem) — empty when no project has snapshots yet.
248
+ monthLabels: z5.array(z5.string()),
249
+ projects: z5.array(projectMetricsSchema)
250
+ });
251
+ var adminEventsListResponseSchema = z5.object({
252
+ events: z5.array(adminEventSummarySchema),
253
+ nextCursor: z5.string().nullable(),
254
+ approxTotal: z5.number(),
255
+ signatureCounts: z5.array(signatureCountSchema).optional()
256
+ });
257
+ var adminEventDetailResponseSchema = z5.object({
258
+ event: adminEventDetailSchema,
259
+ transition: z5.object({
260
+ current: z5.object({
261
+ state: subscriptionStatusSchema.exclude(["grace", "none"]),
262
+ lastEventAt: z5.iso.datetime().nullable()
263
+ }).nullable(),
264
+ wouldApply: wouldApplySchema
265
+ })
266
+ });
267
+ var adminSubjectSchema = z5.object({
268
+ id: z5.string(),
269
+ subjectType: z5.string(),
270
+ externalId: z5.string(),
271
+ providerCustomerId: z5.string().nullable()
272
+ });
273
+ var featureGrantsSchema = z5.record(
274
+ z5.string(),
275
+ z5.union([z5.boolean(), z5.number()])
276
+ );
277
+ var adminSubjectSubscriptionPlanSchema = z5.object({
278
+ key: z5.string(),
279
+ featureGrants: featureGrantsSchema,
280
+ priceInterval: z5.enum(["month", "year"]).nullable()
281
+ });
282
+ var adminSubjectLookupResponseSchema = z5.object({
283
+ subject: adminSubjectSchema,
284
+ entitlements: entitlementsResponseSchema,
285
+ recentEvents: z5.array(adminEventSummarySchema),
286
+ activeOverrides: z5.array(overrideRecordSchema),
287
+ subscriptionPlan: adminSubjectSubscriptionPlanSchema.nullable()
288
+ });
289
+ var adminPriceSchema = z5.object({
290
+ id: z5.string(),
291
+ interval: z5.enum(["month", "year"]),
292
+ currency: z5.string(),
293
+ unitAmountCents: z5.number(),
294
+ syncStatus: z5.enum(["synced", "pending", "drifted"]),
295
+ syncedUnitAmountCents: z5.number().nullable()
296
+ });
297
+ var adminPlanSchema = z5.object({
298
+ id: z5.string(),
299
+ key: z5.string(),
300
+ name: z5.string(),
301
+ featureGrants: featureGrantsSchema,
302
+ activeSubscriptions: z5.number(),
303
+ prices: z5.array(adminPriceSchema)
304
+ });
305
+ var adminPlansResponseSchema = z5.object({
306
+ project: z5.object({
307
+ statementDescriptor: z5.string().nullable(),
308
+ statementDescriptorPrefix: z5.string().nullable()
309
+ }),
310
+ plans: z5.array(adminPlanSchema)
311
+ });
312
+ var adminProviderResponseSchema = z5.object({
313
+ projectId: z5.string(),
314
+ providerKind: z5.string(),
315
+ environment: z5.enum(["test", "live"]),
316
+ envLocked: z5.boolean(),
317
+ secretKeyMasked: z5.string().nullable(),
318
+ publishableKeyMasked: z5.string().nullable(),
319
+ webhookEndpoint: z5.string(),
320
+ webhookSigningConfigured: z5.boolean(),
321
+ defaultPortalReturnUrl: z5.string().nullable()
322
+ });
323
+ var adminAuditEntrySchema = overrideRecordSchema.extend({
324
+ projectId: z5.string()
325
+ });
326
+ var adminAuditResponseSchema = z5.object({
327
+ entries: z5.array(adminAuditEntrySchema),
328
+ nextCursor: z5.string().nullable(),
329
+ approxTotal: z5.number()
330
+ });
331
+ var adminThroughputResponseSchema = z5.object({
332
+ days: z5.array(z5.string()),
333
+ processed: z5.array(z5.number()),
334
+ failed: z5.array(z5.number())
335
+ });
336
+ var adminPlanMixResponseSchema = z5.object({
337
+ months: z5.array(z5.string()),
338
+ plans: z5.array(z5.object({ key: z5.string(), name: z5.string() })),
339
+ stacks: z5.array(z5.array(z5.number()))
340
+ });
341
+ var adminDeadLetterGroupSchema = z5.object({
342
+ projectId: z5.string(),
343
+ signature: z5.string(),
344
+ count: z5.number(),
345
+ sampleEventId: z5.string(),
346
+ firstReceivedAt: z5.iso.datetime(),
347
+ lastReceivedAt: z5.iso.datetime()
348
+ });
349
+ var adminDeadLetterGroupsResponseSchema = z5.object({
350
+ groups: z5.array(adminDeadLetterGroupSchema)
351
+ });
352
+ var adminProjectDashboardResponseSchema = z5.object({
353
+ metrics: projectMetricsSchema,
354
+ monthLabels: z5.array(z5.string()),
355
+ providerKind: z5.string(),
356
+ plans: z5.array(adminPlanSchema)
357
+ });
358
+
359
+ // ../shared-types/src/checkout.ts
360
+ import { z as z6 } from "zod";
361
+ var checkoutRequestSchema = z6.object({
362
+ planKey: z6.string(),
363
+ subjectId: z6.string(),
364
+ successUrl: z6.url(),
365
+ cancelUrl: z6.url(),
366
+ email: z6.email().optional()
367
+ });
368
+ var checkoutResponseSchema = z6.object({
369
+ url: z6.url()
370
+ });
371
+ var portalRequestSchema = z6.object({
372
+ subjectId: z6.string(),
373
+ returnUrl: z6.url()
374
+ });
375
+ var portalResponseSchema = z6.object({
376
+ url: z6.url()
377
+ });
378
+
379
+ // ../shared-types/src/projects.ts
380
+ import { z as z7 } from "zod";
381
+ var registerProjectRequestSchema = z7.object({
382
+ key: z7.string().trim().min(1, "is required"),
383
+ name: z7.string().trim().min(1, "is required"),
384
+ subjectType: z7.string().trim().min(1, "is required"),
385
+ environment: z7.enum(["test", "live"], 'must be "test" or "live"'),
386
+ envLocked: z7.boolean().optional(),
387
+ providerKind: z7.string().trim().min(1, "is required"),
388
+ providerSecretKeyRef: z7.string().optional(),
389
+ providerPublishableKeyRef: z7.string().optional(),
390
+ providerPortalConfigRef: z7.string().optional(),
391
+ statementDescriptor: z7.string().optional(),
392
+ statementDescriptorPrefix: z7.string().optional(),
393
+ graceDays: z7.number().min(0, "must be >= 0").optional(),
394
+ defaultPortalReturnUrl: z7.url().optional()
395
+ }).superRefine((input, ctx) => {
396
+ if (!input.statementDescriptor && !input.statementDescriptorPrefix) {
397
+ ctx.addIssue({
398
+ code: "custom",
399
+ path: ["statementDescriptor"],
400
+ message: "statementDescriptor or statementDescriptorPrefix is required"
401
+ });
402
+ }
403
+ });
404
+
405
+ // ../shared-types/src/errors.ts
406
+ import { z as z8 } from "zod";
407
+ var errorEnvelopeSchema = z8.object({
408
+ error: z8.object({
409
+ code: z8.string(),
410
+ message: z8.string()
411
+ })
412
+ });
413
+
414
+ // ../shared-types/src/webhook-secret.ts
415
+ import { z as z9 } from "zod";
416
+ var webhookSigningSecretSchema = z9.string().regex(/^whsec_.+/, "must be a non-empty whsec_-prefixed string");
417
+
418
+ // ../shared-types/src/portal-return-url.ts
419
+ import { z as z10 } from "zod";
420
+ var portalReturnUrlSchema = z10.url();
421
+
422
+ // src/errors.ts
423
+ var BillingApiError = class extends Error {
424
+ status;
425
+ code;
426
+ constructor(status, code, message) {
427
+ super(message);
428
+ this.name = "BillingApiError";
429
+ this.status = status;
430
+ this.code = code;
431
+ }
432
+ };
433
+ async function errorFromResponse(response) {
434
+ let code = "unknown_error";
435
+ let message = `Request failed with status ${response.status}`;
436
+ try {
437
+ const body = await response.json();
438
+ const parsed = errorEnvelopeSchema.safeParse(body);
439
+ if (parsed.success) {
440
+ code = parsed.data.error.code;
441
+ message = parsed.data.error.message;
442
+ }
443
+ } catch {
444
+ }
445
+ return new BillingApiError(response.status, code, message);
446
+ }
447
+ function networkError(cause) {
448
+ const message = cause instanceof Error ? cause.message : "Network request failed";
449
+ return new BillingApiError(0, "network_error", message);
450
+ }
451
+ function invalidResponseError(message) {
452
+ return new BillingApiError(502, "invalid_response", message);
453
+ }
454
+
455
+ // src/guard.ts
456
+ function isMisconfiguration(err) {
457
+ return err instanceof BillingApiError && err.status >= 400 && err.status < 500;
458
+ }
459
+ function decideOnContent(entitlements, feature, limit) {
460
+ if (feature !== void 0) {
461
+ return entitlements.features[feature] ? { decision: "allow", entitlements } : { decision: "deny", reason: `Feature "${feature}" is not entitled` };
462
+ }
463
+ if (limit !== void 0) {
464
+ const max = entitlements.limits[limit.key];
465
+ return max !== void 0 && limit.usage < max ? { decision: "allow", entitlements } : { decision: "deny", reason: `Limit "${limit.key}" has been reached` };
466
+ }
467
+ return { decision: "allow", entitlements };
468
+ }
469
+ async function checkEntitlement(input) {
470
+ const { client, subjectId, feature, limit, onUnavailable } = input;
471
+ let entitlements;
472
+ try {
473
+ entitlements = await client.getEntitlements(subjectId);
474
+ } catch (err) {
475
+ if (isMisconfiguration(err)) {
476
+ const message = err instanceof Error ? err.message : "Billing request was rejected";
477
+ return { decision: "deny", reason: message, misconfigured: true };
478
+ }
479
+ return onUnavailable === "open" ? { decision: "unavailable-allow" } : { decision: "unavailable-deny", reason: "Billing is unavailable" };
480
+ }
481
+ return decideOnContent(entitlements, feature, limit);
482
+ }
483
+
484
+ export {
485
+ subscriptionViewSchema,
486
+ entitlementsResponseSchema,
487
+ checkoutResponseSchema,
488
+ portalResponseSchema,
489
+ BillingApiError,
490
+ errorFromResponse,
491
+ networkError,
492
+ invalidResponseError,
493
+ checkEntitlement
494
+ };
495
+ //# sourceMappingURL=chunk-3OSJ6UJT.js.map