@vex-chat/spire 2.4.0 → 2.6.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.
Files changed (33) hide show
  1. package/dist/BillingVerification.d.ts +40 -0
  2. package/dist/BillingVerification.js +535 -0
  3. package/dist/BillingVerification.js.map +1 -0
  4. package/dist/Database.d.ts +39 -1
  5. package/dist/Database.js +285 -2
  6. package/dist/Database.js.map +1 -1
  7. package/dist/db/schema.d.ts +50 -0
  8. package/dist/migrations/2026-06-24_account-entitlements.d.ts +8 -0
  9. package/dist/migrations/2026-06-24_account-entitlements.js +20 -0
  10. package/dist/migrations/2026-06-24_account-entitlements.js.map +1 -0
  11. package/dist/migrations/2026-07-02_store-subscriptions.d.ts +8 -0
  12. package/dist/migrations/2026-07-02_store-subscriptions.js +83 -0
  13. package/dist/migrations/2026-07-02_store-subscriptions.js.map +1 -0
  14. package/dist/server/billing.d.ts +14 -0
  15. package/dist/server/billing.js +234 -0
  16. package/dist/server/billing.js.map +1 -0
  17. package/dist/server/entitlements.d.ts +8 -0
  18. package/dist/server/entitlements.js +71 -0
  19. package/dist/server/entitlements.js.map +1 -0
  20. package/dist/server/index.js +6 -0
  21. package/dist/server/index.js.map +1 -1
  22. package/package.json +4 -4
  23. package/src/BillingVerification.ts +800 -0
  24. package/src/Database.ts +422 -2
  25. package/src/__tests__/Database.spec.ts +6 -3
  26. package/src/__tests__/billing.spec.ts +282 -0
  27. package/src/__tests__/entitlements.spec.ts +241 -0
  28. package/src/db/schema.ts +66 -1
  29. package/src/migrations/2026-06-24_account-entitlements.ts +23 -0
  30. package/src/migrations/2026-07-02_store-subscriptions.ts +92 -0
  31. package/src/server/billing.ts +361 -0
  32. package/src/server/entitlements.ts +104 -0
  33. package/src/server/index.ts +6 -0
@@ -4,6 +4,50 @@
4
4
  * Commercial licenses available at vex.wtf
5
5
  */
6
6
  import type { Insertable, Selectable, Updateable } from "kysely";
7
+ export type AccountEntitlementRow = Selectable<AccountEntitlementsTable>;
8
+ export interface AccountEntitlementsTable {
9
+ expiresAt: null | string;
10
+ source: string;
11
+ tier: string;
12
+ updatedAt: string;
13
+ userID: string;
14
+ }
15
+ export type AccountEntitlementUpdate = Updateable<AccountEntitlementsTable>;
16
+ export type BillingStoreSubscriptionRow = Selectable<BillingStoreSubscriptionsTable>;
17
+ export interface BillingStoreSubscriptionsTable {
18
+ createdAt: string;
19
+ environment: string;
20
+ expiresAt: null | string;
21
+ externalOriginalID: null | string;
22
+ externalTransactionID: null | string;
23
+ platform: string;
24
+ productID: string;
25
+ purchaseToken: null | string;
26
+ purchaseTokenHash: null | string;
27
+ rawPayload: string;
28
+ status: string;
29
+ storeProductID: string;
30
+ subscriptionID: string;
31
+ tier: string;
32
+ updatedAt: string;
33
+ userID: string;
34
+ }
35
+ export type BillingStoreSubscriptionUpdate = Updateable<BillingStoreSubscriptionsTable>;
36
+ export type BillingStoreTransactionRow = Selectable<BillingStoreTransactionsTable>;
37
+ export interface BillingStoreTransactionsTable {
38
+ environment: string;
39
+ eventType: string;
40
+ externalTransactionID: null | string;
41
+ platform: string;
42
+ processedAt: string;
43
+ purchaseTokenHash: null | string;
44
+ rawPayload: string;
45
+ storeProductID: string;
46
+ subscriptionID: string;
47
+ transactionID: string;
48
+ userID: string;
49
+ }
50
+ export type BillingStoreTransactionUpdate = Updateable<BillingStoreTransactionsTable>;
7
51
  export type ChannelRow = Selectable<ChannelsTable>;
8
52
  export interface ChannelsTable {
9
53
  channelID: string;
@@ -68,6 +112,9 @@ export interface MailTable {
68
112
  time: string;
69
113
  }
70
114
  export type MailUpdate = Updateable<MailTable>;
115
+ export type NewAccountEntitlement = Insertable<AccountEntitlementsTable>;
116
+ export type NewBillingStoreSubscription = Insertable<BillingStoreSubscriptionsTable>;
117
+ export type NewBillingStoreTransaction = Insertable<BillingStoreTransactionsTable>;
71
118
  export type NewChannel = Insertable<ChannelsTable>;
72
119
  export type NewDevice = Insertable<DevicesTable>;
73
120
  export type NewDevicePasskeyApproval = Insertable<DevicePasskeyApprovalsTable>;
@@ -141,6 +188,9 @@ export interface PreKeysTable {
141
188
  }
142
189
  export type PreKeyUpdate = Updateable<PreKeysTable>;
143
190
  export interface ServerDatabase {
191
+ account_entitlements: AccountEntitlementsTable;
192
+ billing_store_subscriptions: BillingStoreSubscriptionsTable;
193
+ billing_store_transactions: BillingStoreTransactionsTable;
144
194
  channels: ChannelsTable;
145
195
  device_passkey_approvals: DevicePasskeyApprovalsTable;
146
196
  devices: DevicesTable;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+ import type { Kysely } from "kysely";
7
+ export declare function down(db: Kysely<unknown>): Promise<void>;
8
+ export declare function up(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+ export async function down(db) {
7
+ await db.schema.dropTable("account_entitlements").ifExists().execute();
8
+ }
9
+ export async function up(db) {
10
+ await db.schema
11
+ .createTable("account_entitlements")
12
+ .ifNotExists()
13
+ .addColumn("userID", "varchar(255)", (cb) => cb.primaryKey())
14
+ .addColumn("tier", "varchar(32)", (cb) => cb.notNull())
15
+ .addColumn("source", "varchar(32)", (cb) => cb.notNull())
16
+ .addColumn("expiresAt", "text")
17
+ .addColumn("updatedAt", "text", (cb) => cb.notNull())
18
+ .execute();
19
+ }
20
+ //# sourceMappingURL=2026-06-24_account-entitlements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"2026-06-24_account-entitlements.js","sourceRoot":"","sources":["../../src/migrations/2026-06-24_account-entitlements.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,EAAmB;IAC1C,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC;AAC3E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC,EAAmB;IACxC,MAAM,EAAE,CAAC,MAAM;SACV,WAAW,CAAC,sBAAsB,CAAC;SACnC,WAAW,EAAE;SACb,SAAS,CAAC,QAAQ,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC;SAC5D,SAAS,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC;SACtD,SAAS,CAAC,QAAQ,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC;SACxD,SAAS,CAAC,WAAW,EAAE,MAAM,CAAC;SAC9B,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC;SACpD,OAAO,EAAE,CAAC;AACnB,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+ import type { Kysely } from "kysely";
7
+ export declare function down(db: Kysely<unknown>): Promise<void>;
8
+ export declare function up(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+ export async function down(db) {
7
+ await db.schema
8
+ .dropTable("billing_store_transactions")
9
+ .ifExists()
10
+ .execute();
11
+ await db.schema
12
+ .dropTable("billing_store_subscriptions")
13
+ .ifExists()
14
+ .execute();
15
+ }
16
+ export async function up(db) {
17
+ await db.schema
18
+ .createTable("billing_store_subscriptions")
19
+ .ifNotExists()
20
+ .addColumn("subscriptionID", "varchar(255)", (cb) => cb.primaryKey())
21
+ .addColumn("userID", "varchar(255)", (cb) => cb.notNull())
22
+ .addColumn("platform", "varchar(32)", (cb) => cb.notNull())
23
+ .addColumn("environment", "varchar(32)", (cb) => cb.notNull())
24
+ .addColumn("productID", "varchar(255)", (cb) => cb.notNull())
25
+ .addColumn("storeProductID", "varchar(255)", (cb) => cb.notNull())
26
+ .addColumn("tier", "varchar(32)", (cb) => cb.notNull())
27
+ .addColumn("status", "varchar(32)", (cb) => cb.notNull())
28
+ .addColumn("expiresAt", "text")
29
+ .addColumn("externalOriginalID", "varchar(255)")
30
+ .addColumn("externalTransactionID", "varchar(255)")
31
+ .addColumn("purchaseToken", "text")
32
+ .addColumn("purchaseTokenHash", "varchar(128)")
33
+ .addColumn("rawPayload", "text", (cb) => cb.notNull())
34
+ .addColumn("createdAt", "text", (cb) => cb.notNull())
35
+ .addColumn("updatedAt", "text", (cb) => cb.notNull())
36
+ .execute();
37
+ await db.schema
38
+ .createIndex("billing_store_subscriptions_user_idx")
39
+ .ifNotExists()
40
+ .on("billing_store_subscriptions")
41
+ .column("userID")
42
+ .execute();
43
+ await db.schema
44
+ .createIndex("billing_store_subscriptions_original_idx")
45
+ .ifNotExists()
46
+ .on("billing_store_subscriptions")
47
+ .columns(["platform", "environment", "externalOriginalID"])
48
+ .execute();
49
+ await db.schema
50
+ .createIndex("billing_store_subscriptions_token_hash_idx")
51
+ .ifNotExists()
52
+ .on("billing_store_subscriptions")
53
+ .columns(["platform", "environment", "purchaseTokenHash"])
54
+ .execute();
55
+ await db.schema
56
+ .createTable("billing_store_transactions")
57
+ .ifNotExists()
58
+ .addColumn("transactionID", "varchar(255)", (cb) => cb.primaryKey())
59
+ .addColumn("subscriptionID", "varchar(255)", (cb) => cb.notNull())
60
+ .addColumn("userID", "varchar(255)", (cb) => cb.notNull())
61
+ .addColumn("platform", "varchar(32)", (cb) => cb.notNull())
62
+ .addColumn("environment", "varchar(32)", (cb) => cb.notNull())
63
+ .addColumn("storeProductID", "varchar(255)", (cb) => cb.notNull())
64
+ .addColumn("eventType", "varchar(64)", (cb) => cb.notNull())
65
+ .addColumn("externalTransactionID", "varchar(255)")
66
+ .addColumn("purchaseTokenHash", "varchar(128)")
67
+ .addColumn("rawPayload", "text", (cb) => cb.notNull())
68
+ .addColumn("processedAt", "text", (cb) => cb.notNull())
69
+ .execute();
70
+ await db.schema
71
+ .createIndex("billing_store_transactions_subscription_idx")
72
+ .ifNotExists()
73
+ .on("billing_store_transactions")
74
+ .column("subscriptionID")
75
+ .execute();
76
+ await db.schema
77
+ .createIndex("billing_store_transactions_user_idx")
78
+ .ifNotExists()
79
+ .on("billing_store_transactions")
80
+ .column("userID")
81
+ .execute();
82
+ }
83
+ //# sourceMappingURL=2026-07-02_store-subscriptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"2026-07-02_store-subscriptions.js","sourceRoot":"","sources":["../../src/migrations/2026-07-02_store-subscriptions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+ import type { Database } from "../Database.ts";
7
+ export declare function devBillingGrantRoutesEnabled(env?: NodeJS.ProcessEnv): boolean;
8
+ export declare const getBillingRouter: (db: Database, notify: (userID: string, event: string, transmissionID: string, data?: unknown, deviceID?: string) => void) => import("express-serve-static-core").Router;
9
+ export declare function billingAccountPath(): string;
10
+ export declare function billingAppleTransactionPath(): string;
11
+ export declare function billingDevGrantPath(): string;
12
+ export declare function billingGooglePurchasePath(): string;
13
+ export declare function billingProductsPath(): string;
14
+ export declare function billingUserPath(userID: string): string;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+ import express from "express";
7
+ import { AccountTierSchema, AppleServerNotificationRequestSchema, AppleTransactionVerificationRequestSchema, datetime, GooglePlayDeveloperNotificationRequestSchema, GooglePurchaseVerificationRequestSchema, } from "@vex-chat/types";
8
+ import { z } from "zod/v4";
9
+ import { BillingVerificationError, decodeAppleServerNotificationPayload, decodeGooglePlayPubSubNotificationPayload, getBillingProductCatalog, resolveBillingProduct, verifyAppleTransaction, verifyGooglePurchase, } from "../BillingVerification.js";
10
+ import { devApiKeyMatches } from "./rateLimit.js";
11
+ import { getUser } from "./utils.js";
12
+ import { sendWireResponse } from "./wireResponse.js";
13
+ import { protect } from "./index.js";
14
+ const devGrantPayload = z.object({
15
+ expiresAt: datetime.nullable().optional(),
16
+ tier: AccountTierSchema,
17
+ userID: z.string().min(1),
18
+ });
19
+ export function devBillingGrantRoutesEnabled(env = process.env) {
20
+ return (env["NODE_ENV"] !== "production" &&
21
+ env["VEX_ENABLE_DEV_BILLING_GRANTS"] === "1" &&
22
+ (env["DEV_API_KEY"]?.trim().length ?? 0) > 0);
23
+ }
24
+ export const getBillingRouter = (db, notify) => {
25
+ const router = express.Router();
26
+ router.get("/billing/products", protect, (_req, res) => {
27
+ sendWireResponse(_req, res, getBillingProductCatalog());
28
+ });
29
+ router.get("/billing/account", protect, async (req, res) => {
30
+ const userID = getUser(req).userID;
31
+ const state = await db.retrieveBillingAccountState(userID);
32
+ sendWireResponse(req, res, state);
33
+ });
34
+ router.post("/billing/apple/transactions", protect, async (req, res) => {
35
+ const parsed = AppleTransactionVerificationRequestSchema.safeParse(req.body);
36
+ if (!parsed.success) {
37
+ res.status(400).json({
38
+ error: "Invalid Apple transaction payload",
39
+ issues: parsed.error.issues,
40
+ });
41
+ return;
42
+ }
43
+ await handleVerifiedPurchase(req, res, {
44
+ db,
45
+ eventType: "apple_transaction_verified",
46
+ notify,
47
+ purchase: await verifyAppleTransaction(parsed.data),
48
+ userID: getUser(req).userID,
49
+ });
50
+ });
51
+ router.post("/billing/google/purchases", protect, async (req, res) => {
52
+ const parsed = GooglePurchaseVerificationRequestSchema.safeParse(req.body);
53
+ if (!parsed.success) {
54
+ res.status(400).json({
55
+ error: "Invalid Google Play purchase payload",
56
+ issues: parsed.error.issues,
57
+ });
58
+ return;
59
+ }
60
+ await handleVerifiedPurchase(req, res, {
61
+ db,
62
+ eventType: "google_purchase_verified",
63
+ notify,
64
+ purchase: await verifyGooglePurchase(parsed.data),
65
+ userID: getUser(req).userID,
66
+ });
67
+ });
68
+ router.post("/billing/webhooks/apple", async (req, res) => {
69
+ const parsed = AppleServerNotificationRequestSchema.safeParse(req.body);
70
+ if (!parsed.success) {
71
+ res.status(400).json({
72
+ error: "Invalid Apple server notification payload",
73
+ issues: parsed.error.issues,
74
+ });
75
+ return;
76
+ }
77
+ const notification = decodeAppleServerNotificationPayload(parsed.data.signedPayload);
78
+ const signedTransactionInfo = notification.data?.signedTransactionInfo;
79
+ if (!signedTransactionInfo) {
80
+ res.sendStatus(202);
81
+ return;
82
+ }
83
+ const purchase = await verifyAppleTransaction({
84
+ signedTransactionInfo,
85
+ });
86
+ const userID = await db.retrieveStoreSubscriptionOwner({
87
+ environment: purchase.environment,
88
+ externalOriginalID: purchase.externalOriginalID,
89
+ platform: purchase.platform,
90
+ });
91
+ if (!userID) {
92
+ res.sendStatus(202);
93
+ return;
94
+ }
95
+ await handleVerifiedPurchase(req, res, {
96
+ db,
97
+ eventType: `apple_webhook_${notification.notificationType ?? "unknown"}`,
98
+ notify,
99
+ purchase,
100
+ userID,
101
+ });
102
+ });
103
+ router.post("/billing/webhooks/google", async (req, res) => {
104
+ const parsed = GooglePlayDeveloperNotificationRequestSchema.safeParse(req.body);
105
+ if (!parsed.success) {
106
+ res.status(400).json({
107
+ error: "Invalid Google Play notification payload",
108
+ issues: parsed.error.issues,
109
+ });
110
+ return;
111
+ }
112
+ const notification = decodeGooglePlayPubSubNotificationPayload(req.body);
113
+ const purchaseToken = notification.subscriptionNotification?.purchaseToken;
114
+ if (!purchaseToken) {
115
+ res.sendStatus(202);
116
+ return;
117
+ }
118
+ const purchase = await verifyGooglePurchase({
119
+ packageName: notification.packageName,
120
+ productID: notification.subscriptionNotification?.subscriptionId,
121
+ purchaseToken,
122
+ });
123
+ const userID = await db.retrieveStoreSubscriptionOwner({
124
+ environment: purchase.environment,
125
+ platform: purchase.platform,
126
+ purchaseToken,
127
+ });
128
+ if (!userID) {
129
+ res.sendStatus(202);
130
+ return;
131
+ }
132
+ await handleVerifiedPurchase(req, res, {
133
+ db,
134
+ eventType: "google_webhook_subscription",
135
+ notify,
136
+ purchase,
137
+ userID,
138
+ });
139
+ });
140
+ if (devBillingGrantRoutesEnabled()) {
141
+ router.post("/__dev/billing/grants", async (req, res) => {
142
+ if (!devBillingGrantRoutesEnabled()) {
143
+ res.sendStatus(404);
144
+ return;
145
+ }
146
+ if (!devApiKeyMatches(req)) {
147
+ res.sendStatus(403);
148
+ return;
149
+ }
150
+ const parsed = devGrantPayload.safeParse(req.body);
151
+ if (!parsed.success) {
152
+ res.status(400).json({
153
+ error: "Invalid dev billing grant payload",
154
+ issues: parsed.error.issues,
155
+ });
156
+ return;
157
+ }
158
+ const entitlements = await db.setAccountEntitlementTier(parsed.data.userID, parsed.data.tier, {
159
+ expiresAt: parsed.data.expiresAt ?? null,
160
+ source: "store",
161
+ });
162
+ notify(parsed.data.userID, "accountEntitlementsChanged", crypto.randomUUID(), { tier: entitlements.tier });
163
+ sendWireResponse(req, res, entitlements);
164
+ });
165
+ }
166
+ router.use((err, _req, res, next) => {
167
+ if (err instanceof BillingVerificationError) {
168
+ res.status(err.status).json({ error: err.message });
169
+ return;
170
+ }
171
+ next(err);
172
+ });
173
+ return router;
174
+ };
175
+ export function billingAccountPath() {
176
+ return "/billing/account";
177
+ }
178
+ export function billingAppleTransactionPath() {
179
+ return "/billing/apple/transactions";
180
+ }
181
+ export function billingDevGrantPath() {
182
+ return "/__dev/billing/grants";
183
+ }
184
+ export function billingGooglePurchasePath() {
185
+ return "/billing/google/purchases";
186
+ }
187
+ export function billingProductsPath() {
188
+ return "/billing/products";
189
+ }
190
+ export function billingUserPath(userID) {
191
+ return `/user/${encodeURIComponent(userID)}/billing`;
192
+ }
193
+ async function handleVerifiedPurchase(req, res, args) {
194
+ const product = resolveBillingProduct(args.purchase);
195
+ const subscription = await persistVerifiedPurchase(args.db, {
196
+ product,
197
+ purchase: args.purchase,
198
+ userID: args.userID,
199
+ });
200
+ await args.db.recordStoreTransaction({
201
+ eventType: args.eventType,
202
+ externalTransactionID: args.purchase.externalTransactionID,
203
+ rawPayload: args.purchase.rawPayload,
204
+ subscriptionID: subscription.subscriptionID,
205
+ userID: args.userID,
206
+ });
207
+ const entitlements = await args.db.recalculateStoreEntitlements(args.userID);
208
+ args.notify(args.userID, "accountEntitlementsChanged", crypto.randomUUID(), {
209
+ tier: entitlements.tier,
210
+ });
211
+ const state = {
212
+ entitlements,
213
+ subscriptions: await args.db.retrieveBillingSubscriptions(args.userID),
214
+ };
215
+ sendWireResponse(req, res, state);
216
+ }
217
+ async function persistVerifiedPurchase(db, args) {
218
+ const input = {
219
+ environment: args.purchase.environment,
220
+ expiresAt: args.purchase.expiresAt,
221
+ externalOriginalID: args.purchase.externalOriginalID,
222
+ externalTransactionID: args.purchase.externalTransactionID,
223
+ platform: args.purchase.platform,
224
+ productID: args.product.productID,
225
+ purchaseToken: args.purchase.purchaseToken,
226
+ rawPayload: args.purchase.rawPayload,
227
+ status: args.purchase.status,
228
+ storeProductID: args.product.storeProductID,
229
+ tier: args.product.tier,
230
+ userID: args.userID,
231
+ };
232
+ return db.upsertStoreSubscription(input);
233
+ }
234
+ //# sourceMappingURL=billing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.js","sourceRoot":"","sources":["../../src/server/billing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+ import type { Database } from "../Database.ts";
7
+ export declare function devEntitlementRoutesEnabled(env?: NodeJS.ProcessEnv): boolean;
8
+ export declare const getEntitlementRouter: (db: Database, notify: (userID: string, event: string, transmissionID: string, data?: unknown, deviceID?: string) => void) => import("express-serve-static-core").Router;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+ import express from "express";
7
+ import { AccountTierSchema, datetime } from "@vex-chat/types";
8
+ import { z } from "zod/v4";
9
+ import { devApiKeyMatches } from "./rateLimit.js";
10
+ import { getParam, getUser } from "./utils.js";
11
+ import { sendWireResponse } from "./wireResponse.js";
12
+ import { protect } from "./index.js";
13
+ const devEntitlementPatchPayload = z.object({
14
+ expiresAt: datetime.nullable().optional(),
15
+ tier: AccountTierSchema,
16
+ });
17
+ export function devEntitlementRoutesEnabled(env = process.env) {
18
+ return (env["NODE_ENV"] !== "production" &&
19
+ env["VEX_ENABLE_DEV_ENTITLEMENTS"] === "1" &&
20
+ (env["DEV_API_KEY"]?.trim().length ?? 0) > 0);
21
+ }
22
+ export const getEntitlementRouter = (db, notify) => {
23
+ const router = express.Router();
24
+ router.get("/user/:id/entitlements", protect, async (req, res) => {
25
+ const userDetails = getUser(req);
26
+ const userID = getParam(req, "id");
27
+ if (userDetails.userID !== userID) {
28
+ res.sendStatus(401);
29
+ return;
30
+ }
31
+ const entitlements = await db.retrieveAccountEntitlements(userID);
32
+ sendWireResponse(req, res, entitlements);
33
+ });
34
+ if (!devEntitlementRoutesEnabled()) {
35
+ return router;
36
+ }
37
+ router.patch("/__dev/user/:id/entitlements", protect, async (req, res) => {
38
+ if (!devEntitlementRoutesEnabled()) {
39
+ res.sendStatus(404);
40
+ return;
41
+ }
42
+ const userDetails = getUser(req);
43
+ const userID = getParam(req, "id");
44
+ if (userDetails.userID !== userID) {
45
+ res.sendStatus(401);
46
+ return;
47
+ }
48
+ if (!devApiKeyMatches(req)) {
49
+ res.sendStatus(403);
50
+ return;
51
+ }
52
+ const parsed = devEntitlementPatchPayload.safeParse(req.body);
53
+ if (!parsed.success) {
54
+ res.status(400).json({
55
+ error: "Invalid entitlement payload",
56
+ issues: parsed.error.issues,
57
+ });
58
+ return;
59
+ }
60
+ const entitlements = await db.setAccountEntitlementTier(userID, parsed.data.tier, {
61
+ expiresAt: parsed.data.expiresAt ?? null,
62
+ source: "dev_override",
63
+ });
64
+ notify(userID, "accountEntitlementsChanged", crypto.randomUUID(), {
65
+ tier: entitlements.tier,
66
+ });
67
+ sendWireResponse(req, res, entitlements);
68
+ });
69
+ return router;
70
+ };
71
+ //# sourceMappingURL=entitlements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entitlements.js","sourceRoot":"","sources":["../../src/server/entitlements.ts"],"names":[],"mappings":"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"}
@@ -23,7 +23,9 @@ import { msgpack } from "../utils/msgpack.js";
23
23
  import { verifyPreKeyWsSignature } from "../utils/preKeySignature.js";
24
24
  import { spireXSignOpenAsync } from "../utils/spireXSignOpenAsync.js";
25
25
  import { getAvatarRouter } from "./avatar.js";
26
+ import { getBillingRouter } from "./billing.js";
26
27
  import { getCliPasskeyPageRouter } from "./cliPasskeyPage.js";
28
+ import { getEntitlementRouter } from "./entitlements.js";
27
29
  import { errorHandler } from "./errors.js";
28
30
  import { getFileRouter } from "./file.js";
29
31
  import { getInviteRouter } from "./invite.js";
@@ -198,6 +200,8 @@ export const initApp = (api, db, tokenValidator, signKeys, notify, disconnectDev
198
200
  const userRouter = getUserRouter(db, tokenValidator, notify);
199
201
  const fileRouter = getFileRouter(db);
200
202
  const avatarRouter = getAvatarRouter();
203
+ const billingRouter = getBillingRouter(db, notify);
204
+ const entitlementRouter = getEntitlementRouter(db, notify);
201
205
  const inviteRouter = getInviteRouter(db, tokenValidator, notify);
202
206
  const passkeyRouter = getPasskeyRouter(db);
203
207
  const passkeyDeviceRouter = getPasskeyDeviceRouter(db, notify, disconnectDevices);
@@ -776,6 +780,8 @@ export const initApp = (api, db, tokenValidator, signKeys, notify, disconnectDev
776
780
  // an authenticated device) and `/auth/passkey/...` (public
777
781
  // sign-in). The router itself defines the full path on each
778
782
  // route handler.
783
+ api.use(billingRouter);
784
+ api.use(entitlementRouter);
779
785
  api.use(passkeyRouter);
780
786
  api.use(passkeyDeviceRouter);
781
787
  api.use("/user", userRouter);