@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 +138 -0
- package/dist/chunk-3OSJ6UJT.js +495 -0
- package/dist/chunk-3OSJ6UJT.js.map +1 -0
- package/dist/fastify.cjs +556 -0
- package/dist/fastify.cjs.map +1 -0
- package/dist/fastify.d.cts +26 -0
- package/dist/fastify.d.ts +26 -0
- package/dist/fastify.js +71 -0
- package/dist/fastify.js.map +1 -0
- package/dist/index.cjs +625 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/types-D_vVr3D2.d.cts +116 -0
- package/dist/types-D_vVr3D2.d.ts +116 -0
- package/package.json +39 -0
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
|