@vex-chat/spire 2.5.0 → 3.0.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 (54) 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/ClientManager.js +1 -8
  5. package/dist/ClientManager.js.map +1 -1
  6. package/dist/Database.d.ts +41 -4
  7. package/dist/Database.js +291 -14
  8. package/dist/Database.js.map +1 -1
  9. package/dist/NotificationService.d.ts +0 -2
  10. package/dist/NotificationService.js +4 -321
  11. package/dist/NotificationService.js.map +1 -1
  12. package/dist/Spire.js +43 -52
  13. package/dist/Spire.js.map +1 -1
  14. package/dist/db/schema.d.ts +50 -0
  15. package/dist/migrations/2026-06-24_account-entitlements.d.ts +8 -0
  16. package/dist/migrations/2026-06-24_account-entitlements.js +20 -0
  17. package/dist/migrations/2026-06-24_account-entitlements.js.map +1 -0
  18. package/dist/migrations/2026-07-02_store-subscriptions.d.ts +8 -0
  19. package/dist/migrations/2026-07-02_store-subscriptions.js +83 -0
  20. package/dist/migrations/2026-07-02_store-subscriptions.js.map +1 -0
  21. package/dist/server/billing.d.ts +14 -0
  22. package/dist/server/billing.js +234 -0
  23. package/dist/server/billing.js.map +1 -0
  24. package/dist/server/cliPasskeyPage.js +1 -1
  25. package/dist/server/entitlements.d.ts +8 -0
  26. package/dist/server/entitlements.js +71 -0
  27. package/dist/server/entitlements.js.map +1 -0
  28. package/dist/server/index.js +7 -11
  29. package/dist/server/index.js.map +1 -1
  30. package/dist/server/passkey.js +3 -4
  31. package/dist/server/passkey.js.map +1 -1
  32. package/package.json +4 -4
  33. package/src/BillingVerification.ts +800 -0
  34. package/src/ClientManager.ts +9 -23
  35. package/src/Database.ts +433 -20
  36. package/src/NotificationService.ts +4 -428
  37. package/src/Spire.ts +77 -103
  38. package/src/__tests__/Database.spec.ts +36 -3
  39. package/src/__tests__/billing.spec.ts +282 -0
  40. package/src/__tests__/connectAuth.spec.ts +120 -0
  41. package/src/__tests__/entitlements.spec.ts +241 -0
  42. package/src/__tests__/notifyFanout.spec.ts +0 -136
  43. package/src/db/schema.ts +66 -1
  44. package/src/migrations/2026-06-24_account-entitlements.ts +23 -0
  45. package/src/migrations/2026-07-02_store-subscriptions.ts +92 -0
  46. package/src/server/billing.ts +361 -0
  47. package/src/server/cliPasskeyPage.ts +1 -1
  48. package/src/server/entitlements.ts +104 -0
  49. package/src/server/index.ts +7 -16
  50. package/src/server/passkey.ts +3 -4
  51. package/dist/server/callWake.d.ts +0 -13
  52. package/dist/server/callWake.js +0 -18
  53. package/dist/server/callWake.js.map +0 -1
  54. package/src/server/callWake.ts +0 -30
@@ -0,0 +1,92 @@
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
+
7
+ import type { Kysely } from "kysely";
8
+
9
+ export async function down(db: Kysely<unknown>): Promise<void> {
10
+ await db.schema
11
+ .dropTable("billing_store_transactions")
12
+ .ifExists()
13
+ .execute();
14
+ await db.schema
15
+ .dropTable("billing_store_subscriptions")
16
+ .ifExists()
17
+ .execute();
18
+ }
19
+
20
+ export async function up(db: Kysely<unknown>): Promise<void> {
21
+ await db.schema
22
+ .createTable("billing_store_subscriptions")
23
+ .ifNotExists()
24
+ .addColumn("subscriptionID", "varchar(255)", (cb) => cb.primaryKey())
25
+ .addColumn("userID", "varchar(255)", (cb) => cb.notNull())
26
+ .addColumn("platform", "varchar(32)", (cb) => cb.notNull())
27
+ .addColumn("environment", "varchar(32)", (cb) => cb.notNull())
28
+ .addColumn("productID", "varchar(255)", (cb) => cb.notNull())
29
+ .addColumn("storeProductID", "varchar(255)", (cb) => cb.notNull())
30
+ .addColumn("tier", "varchar(32)", (cb) => cb.notNull())
31
+ .addColumn("status", "varchar(32)", (cb) => cb.notNull())
32
+ .addColumn("expiresAt", "text")
33
+ .addColumn("externalOriginalID", "varchar(255)")
34
+ .addColumn("externalTransactionID", "varchar(255)")
35
+ .addColumn("purchaseToken", "text")
36
+ .addColumn("purchaseTokenHash", "varchar(128)")
37
+ .addColumn("rawPayload", "text", (cb) => cb.notNull())
38
+ .addColumn("createdAt", "text", (cb) => cb.notNull())
39
+ .addColumn("updatedAt", "text", (cb) => cb.notNull())
40
+ .execute();
41
+
42
+ await db.schema
43
+ .createIndex("billing_store_subscriptions_user_idx")
44
+ .ifNotExists()
45
+ .on("billing_store_subscriptions")
46
+ .column("userID")
47
+ .execute();
48
+
49
+ await db.schema
50
+ .createIndex("billing_store_subscriptions_original_idx")
51
+ .ifNotExists()
52
+ .on("billing_store_subscriptions")
53
+ .columns(["platform", "environment", "externalOriginalID"])
54
+ .execute();
55
+
56
+ await db.schema
57
+ .createIndex("billing_store_subscriptions_token_hash_idx")
58
+ .ifNotExists()
59
+ .on("billing_store_subscriptions")
60
+ .columns(["platform", "environment", "purchaseTokenHash"])
61
+ .execute();
62
+
63
+ await db.schema
64
+ .createTable("billing_store_transactions")
65
+ .ifNotExists()
66
+ .addColumn("transactionID", "varchar(255)", (cb) => cb.primaryKey())
67
+ .addColumn("subscriptionID", "varchar(255)", (cb) => cb.notNull())
68
+ .addColumn("userID", "varchar(255)", (cb) => cb.notNull())
69
+ .addColumn("platform", "varchar(32)", (cb) => cb.notNull())
70
+ .addColumn("environment", "varchar(32)", (cb) => cb.notNull())
71
+ .addColumn("storeProductID", "varchar(255)", (cb) => cb.notNull())
72
+ .addColumn("eventType", "varchar(64)", (cb) => cb.notNull())
73
+ .addColumn("externalTransactionID", "varchar(255)")
74
+ .addColumn("purchaseTokenHash", "varchar(128)")
75
+ .addColumn("rawPayload", "text", (cb) => cb.notNull())
76
+ .addColumn("processedAt", "text", (cb) => cb.notNull())
77
+ .execute();
78
+
79
+ await db.schema
80
+ .createIndex("billing_store_transactions_subscription_idx")
81
+ .ifNotExists()
82
+ .on("billing_store_transactions")
83
+ .column("subscriptionID")
84
+ .execute();
85
+
86
+ await db.schema
87
+ .createIndex("billing_store_transactions_user_idx")
88
+ .ifNotExists()
89
+ .on("billing_store_transactions")
90
+ .column("userID")
91
+ .execute();
92
+ }
@@ -0,0 +1,361 @@
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
+
7
+ import type { Database, StoreSubscriptionUpsertInput } from "../Database.ts";
8
+ import type {
9
+ BillingAccountState,
10
+ BillingProduct,
11
+ BillingSubscription,
12
+ } from "@vex-chat/types";
13
+
14
+ import express from "express";
15
+
16
+ import {
17
+ AccountTierSchema,
18
+ AppleServerNotificationRequestSchema,
19
+ AppleTransactionVerificationRequestSchema,
20
+ datetime,
21
+ GooglePlayDeveloperNotificationRequestSchema,
22
+ GooglePurchaseVerificationRequestSchema,
23
+ } from "@vex-chat/types";
24
+
25
+ import { z } from "zod/v4";
26
+
27
+ import {
28
+ BillingVerificationError,
29
+ decodeAppleServerNotificationPayload,
30
+ decodeGooglePlayPubSubNotificationPayload,
31
+ getBillingProductCatalog,
32
+ resolveBillingProduct,
33
+ type VerifiedStorePurchase,
34
+ verifyAppleTransaction,
35
+ verifyGooglePurchase,
36
+ } from "../BillingVerification.ts";
37
+
38
+ import { devApiKeyMatches } from "./rateLimit.ts";
39
+ import { getUser } from "./utils.ts";
40
+ import { sendWireResponse } from "./wireResponse.ts";
41
+
42
+ import { protect } from "./index.ts";
43
+
44
+ const devGrantPayload = z.object({
45
+ expiresAt: datetime.nullable().optional(),
46
+ tier: AccountTierSchema,
47
+ userID: z.string().min(1),
48
+ });
49
+
50
+ export function devBillingGrantRoutesEnabled(
51
+ env: NodeJS.ProcessEnv = process.env,
52
+ ): boolean {
53
+ return (
54
+ env["NODE_ENV"] !== "production" &&
55
+ env["VEX_ENABLE_DEV_BILLING_GRANTS"] === "1" &&
56
+ (env["DEV_API_KEY"]?.trim().length ?? 0) > 0
57
+ );
58
+ }
59
+
60
+ export const getBillingRouter = (
61
+ db: Database,
62
+ notify: (
63
+ userID: string,
64
+ event: string,
65
+ transmissionID: string,
66
+ data?: unknown,
67
+ deviceID?: string,
68
+ ) => void,
69
+ ) => {
70
+ const router = express.Router();
71
+
72
+ router.get("/billing/products", protect, (_req, res) => {
73
+ sendWireResponse(_req, res, getBillingProductCatalog());
74
+ });
75
+
76
+ router.get("/billing/account", protect, async (req, res) => {
77
+ const userID = getUser(req).userID;
78
+ const state = await db.retrieveBillingAccountState(userID);
79
+ sendWireResponse(req, res, state);
80
+ });
81
+
82
+ router.post("/billing/apple/transactions", protect, async (req, res) => {
83
+ const parsed = AppleTransactionVerificationRequestSchema.safeParse(
84
+ req.body,
85
+ );
86
+ if (!parsed.success) {
87
+ res.status(400).json({
88
+ error: "Invalid Apple transaction payload",
89
+ issues: parsed.error.issues,
90
+ });
91
+ return;
92
+ }
93
+
94
+ await handleVerifiedPurchase(req, res, {
95
+ db,
96
+ eventType: "apple_transaction_verified",
97
+ notify,
98
+ purchase: await verifyAppleTransaction(parsed.data),
99
+ userID: getUser(req).userID,
100
+ });
101
+ });
102
+
103
+ router.post("/billing/google/purchases", protect, async (req, res) => {
104
+ const parsed = GooglePurchaseVerificationRequestSchema.safeParse(
105
+ req.body,
106
+ );
107
+ if (!parsed.success) {
108
+ res.status(400).json({
109
+ error: "Invalid Google Play purchase payload",
110
+ issues: parsed.error.issues,
111
+ });
112
+ return;
113
+ }
114
+
115
+ await handleVerifiedPurchase(req, res, {
116
+ db,
117
+ eventType: "google_purchase_verified",
118
+ notify,
119
+ purchase: await verifyGooglePurchase(parsed.data),
120
+ userID: getUser(req).userID,
121
+ });
122
+ });
123
+
124
+ router.post("/billing/webhooks/apple", async (req, res) => {
125
+ const parsed = AppleServerNotificationRequestSchema.safeParse(req.body);
126
+ if (!parsed.success) {
127
+ res.status(400).json({
128
+ error: "Invalid Apple server notification payload",
129
+ issues: parsed.error.issues,
130
+ });
131
+ return;
132
+ }
133
+
134
+ const notification = decodeAppleServerNotificationPayload(
135
+ parsed.data.signedPayload,
136
+ );
137
+ const signedTransactionInfo = notification.data?.signedTransactionInfo;
138
+ if (!signedTransactionInfo) {
139
+ res.sendStatus(202);
140
+ return;
141
+ }
142
+
143
+ const purchase = await verifyAppleTransaction({
144
+ signedTransactionInfo,
145
+ });
146
+ const userID = await db.retrieveStoreSubscriptionOwner({
147
+ environment: purchase.environment,
148
+ externalOriginalID: purchase.externalOriginalID,
149
+ platform: purchase.platform,
150
+ });
151
+ if (!userID) {
152
+ res.sendStatus(202);
153
+ return;
154
+ }
155
+
156
+ await handleVerifiedPurchase(req, res, {
157
+ db,
158
+ eventType: `apple_webhook_${notification.notificationType ?? "unknown"}`,
159
+ notify,
160
+ purchase,
161
+ userID,
162
+ });
163
+ });
164
+
165
+ router.post("/billing/webhooks/google", async (req, res) => {
166
+ const parsed = GooglePlayDeveloperNotificationRequestSchema.safeParse(
167
+ req.body,
168
+ );
169
+ if (!parsed.success) {
170
+ res.status(400).json({
171
+ error: "Invalid Google Play notification payload",
172
+ issues: parsed.error.issues,
173
+ });
174
+ return;
175
+ }
176
+ const notification = decodeGooglePlayPubSubNotificationPayload(
177
+ req.body,
178
+ );
179
+ const purchaseToken =
180
+ notification.subscriptionNotification?.purchaseToken;
181
+ if (!purchaseToken) {
182
+ res.sendStatus(202);
183
+ return;
184
+ }
185
+
186
+ const purchase = await verifyGooglePurchase({
187
+ packageName: notification.packageName,
188
+ productID: notification.subscriptionNotification?.subscriptionId,
189
+ purchaseToken,
190
+ });
191
+ const userID = await db.retrieveStoreSubscriptionOwner({
192
+ environment: purchase.environment,
193
+ platform: purchase.platform,
194
+ purchaseToken,
195
+ });
196
+ if (!userID) {
197
+ res.sendStatus(202);
198
+ return;
199
+ }
200
+
201
+ await handleVerifiedPurchase(req, res, {
202
+ db,
203
+ eventType: "google_webhook_subscription",
204
+ notify,
205
+ purchase,
206
+ userID,
207
+ });
208
+ });
209
+
210
+ if (devBillingGrantRoutesEnabled()) {
211
+ router.post("/__dev/billing/grants", async (req, res) => {
212
+ if (!devBillingGrantRoutesEnabled()) {
213
+ res.sendStatus(404);
214
+ return;
215
+ }
216
+ if (!devApiKeyMatches(req)) {
217
+ res.sendStatus(403);
218
+ return;
219
+ }
220
+ const parsed = devGrantPayload.safeParse(req.body);
221
+ if (!parsed.success) {
222
+ res.status(400).json({
223
+ error: "Invalid dev billing grant payload",
224
+ issues: parsed.error.issues,
225
+ });
226
+ return;
227
+ }
228
+
229
+ const entitlements = await db.setAccountEntitlementTier(
230
+ parsed.data.userID,
231
+ parsed.data.tier,
232
+ {
233
+ expiresAt: parsed.data.expiresAt ?? null,
234
+ source: "store",
235
+ },
236
+ );
237
+ notify(
238
+ parsed.data.userID,
239
+ "accountEntitlementsChanged",
240
+ crypto.randomUUID(),
241
+ { tier: entitlements.tier },
242
+ );
243
+ sendWireResponse(req, res, entitlements);
244
+ });
245
+ }
246
+
247
+ router.use(
248
+ (
249
+ err: unknown,
250
+ _req: express.Request,
251
+ res: express.Response,
252
+ next: express.NextFunction,
253
+ ) => {
254
+ if (err instanceof BillingVerificationError) {
255
+ res.status(err.status).json({ error: err.message });
256
+ return;
257
+ }
258
+ next(err);
259
+ },
260
+ );
261
+
262
+ return router;
263
+ };
264
+
265
+ export function billingAccountPath(): string {
266
+ return "/billing/account";
267
+ }
268
+
269
+ export function billingAppleTransactionPath(): string {
270
+ return "/billing/apple/transactions";
271
+ }
272
+
273
+ export function billingDevGrantPath(): string {
274
+ return "/__dev/billing/grants";
275
+ }
276
+
277
+ export function billingGooglePurchasePath(): string {
278
+ return "/billing/google/purchases";
279
+ }
280
+
281
+ export function billingProductsPath(): string {
282
+ return "/billing/products";
283
+ }
284
+
285
+ export function billingUserPath(userID: string): string {
286
+ return `/user/${encodeURIComponent(userID)}/billing`;
287
+ }
288
+
289
+ async function handleVerifiedPurchase(
290
+ req: express.Request,
291
+ res: express.Response,
292
+ args: {
293
+ db: Database;
294
+ eventType: string;
295
+ notify: (
296
+ userID: string,
297
+ event: string,
298
+ transmissionID: string,
299
+ data?: unknown,
300
+ deviceID?: string,
301
+ ) => void;
302
+ purchase: VerifiedStorePurchase;
303
+ userID: string;
304
+ },
305
+ ): Promise<void> {
306
+ const product = resolveBillingProduct(args.purchase);
307
+ const subscription = await persistVerifiedPurchase(args.db, {
308
+ product,
309
+ purchase: args.purchase,
310
+ userID: args.userID,
311
+ });
312
+ await args.db.recordStoreTransaction({
313
+ eventType: args.eventType,
314
+ externalTransactionID: args.purchase.externalTransactionID,
315
+ rawPayload: args.purchase.rawPayload,
316
+ subscriptionID: subscription.subscriptionID,
317
+ userID: args.userID,
318
+ });
319
+ const entitlements = await args.db.recalculateStoreEntitlements(
320
+ args.userID,
321
+ );
322
+ args.notify(
323
+ args.userID,
324
+ "accountEntitlementsChanged",
325
+ crypto.randomUUID(),
326
+ {
327
+ tier: entitlements.tier,
328
+ },
329
+ );
330
+
331
+ const state: BillingAccountState = {
332
+ entitlements,
333
+ subscriptions: await args.db.retrieveBillingSubscriptions(args.userID),
334
+ };
335
+ sendWireResponse(req, res, state);
336
+ }
337
+
338
+ async function persistVerifiedPurchase(
339
+ db: Database,
340
+ args: {
341
+ product: BillingProduct;
342
+ purchase: VerifiedStorePurchase;
343
+ userID: string;
344
+ },
345
+ ): Promise<BillingSubscription> {
346
+ const input: StoreSubscriptionUpsertInput = {
347
+ environment: args.purchase.environment,
348
+ expiresAt: args.purchase.expiresAt,
349
+ externalOriginalID: args.purchase.externalOriginalID,
350
+ externalTransactionID: args.purchase.externalTransactionID,
351
+ platform: args.purchase.platform,
352
+ productID: args.product.productID,
353
+ purchaseToken: args.purchase.purchaseToken,
354
+ rawPayload: args.purchase.rawPayload,
355
+ status: args.purchase.status,
356
+ storeProductID: args.product.storeProductID,
357
+ tier: args.product.tier,
358
+ userID: args.userID,
359
+ };
360
+ return db.upsertStoreSubscription(input);
361
+ }
@@ -167,7 +167,7 @@ const CLI_PASSKEY_PAGE = `<!doctype html>
167
167
 
168
168
  if (mode === "register") {
169
169
  titleEl.textContent = "Create a passkey for Vex.";
170
- copyEl.textContent = "This adds the first passkey required by your new CLI account.";
170
+ copyEl.textContent = "This adds a passkey to your Vex account.";
171
171
  action.textContent = "Create passkey";
172
172
  setStatus("Ready to create passkey.");
173
173
  } else {
@@ -0,0 +1,104 @@
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
+
7
+ import type { Database } from "../Database.ts";
8
+
9
+ import express from "express";
10
+
11
+ import { AccountTierSchema, datetime } from "@vex-chat/types";
12
+
13
+ import { z } from "zod/v4";
14
+
15
+ import { devApiKeyMatches } from "./rateLimit.ts";
16
+ import { getParam, getUser } from "./utils.ts";
17
+ import { sendWireResponse } from "./wireResponse.ts";
18
+
19
+ import { protect } from "./index.ts";
20
+
21
+ const devEntitlementPatchPayload = z.object({
22
+ expiresAt: datetime.nullable().optional(),
23
+ tier: AccountTierSchema,
24
+ });
25
+
26
+ export function devEntitlementRoutesEnabled(
27
+ env: NodeJS.ProcessEnv = process.env,
28
+ ): boolean {
29
+ return (
30
+ env["NODE_ENV"] !== "production" &&
31
+ env["VEX_ENABLE_DEV_ENTITLEMENTS"] === "1" &&
32
+ (env["DEV_API_KEY"]?.trim().length ?? 0) > 0
33
+ );
34
+ }
35
+
36
+ export const getEntitlementRouter = (
37
+ db: Database,
38
+ notify: (
39
+ userID: string,
40
+ event: string,
41
+ transmissionID: string,
42
+ data?: unknown,
43
+ deviceID?: string,
44
+ ) => void,
45
+ ) => {
46
+ const router = express.Router();
47
+
48
+ router.get("/user/:id/entitlements", protect, async (req, res) => {
49
+ const userDetails = getUser(req);
50
+ const userID = getParam(req, "id");
51
+ if (userDetails.userID !== userID) {
52
+ res.sendStatus(401);
53
+ return;
54
+ }
55
+
56
+ const entitlements = await db.retrieveAccountEntitlements(userID);
57
+ sendWireResponse(req, res, entitlements);
58
+ });
59
+
60
+ if (!devEntitlementRoutesEnabled()) {
61
+ return router;
62
+ }
63
+
64
+ router.patch("/__dev/user/:id/entitlements", protect, async (req, res) => {
65
+ if (!devEntitlementRoutesEnabled()) {
66
+ res.sendStatus(404);
67
+ return;
68
+ }
69
+ const userDetails = getUser(req);
70
+ const userID = getParam(req, "id");
71
+ if (userDetails.userID !== userID) {
72
+ res.sendStatus(401);
73
+ return;
74
+ }
75
+ if (!devApiKeyMatches(req)) {
76
+ res.sendStatus(403);
77
+ return;
78
+ }
79
+
80
+ const parsed = devEntitlementPatchPayload.safeParse(req.body);
81
+ if (!parsed.success) {
82
+ res.status(400).json({
83
+ error: "Invalid entitlement payload",
84
+ issues: parsed.error.issues,
85
+ });
86
+ return;
87
+ }
88
+
89
+ const entitlements = await db.setAccountEntitlementTier(
90
+ userID,
91
+ parsed.data.tier,
92
+ {
93
+ expiresAt: parsed.data.expiresAt ?? null,
94
+ source: "dev_override",
95
+ },
96
+ );
97
+ notify(userID, "accountEntitlementsChanged", crypto.randomUUID(), {
98
+ tier: entitlements.tier,
99
+ });
100
+ sendWireResponse(req, res, entitlements);
101
+ });
102
+
103
+ return router;
104
+ };
@@ -32,7 +32,9 @@ import { verifyPreKeyWsSignature } from "../utils/preKeySignature.ts";
32
32
  import { spireXSignOpenAsync } from "../utils/spireXSignOpenAsync.ts";
33
33
 
34
34
  import { getAvatarRouter } from "./avatar.ts";
35
+ import { getBillingRouter } from "./billing.ts";
35
36
  import { getCliPasskeyPageRouter } from "./cliPasskeyPage.ts";
37
+ import { getEntitlementRouter } from "./entitlements.ts";
36
38
  import { errorHandler } from "./errors.ts";
37
39
  import { getFileRouter } from "./file.ts";
38
40
  import { getInviteRouter } from "./invite.ts";
@@ -44,11 +46,7 @@ import {
44
46
  hasPermission,
45
47
  userHasPermission,
46
48
  } from "./permissions.ts";
47
- import {
48
- devApiKeyMatches,
49
- globalLimiter,
50
- keyBundleLimiter,
51
- } from "./rateLimit.ts";
49
+ import { globalLimiter, keyBundleLimiter } from "./rateLimit.ts";
52
50
  import { getUserRouter } from "./user.ts";
53
51
  import { censorUser, getParam, getUser } from "./utils.ts";
54
52
  import { getWellKnownRouter } from "./wellKnown.ts";
@@ -255,6 +253,8 @@ export const initApp = (
255
253
  const userRouter = getUserRouter(db, tokenValidator, notify);
256
254
  const fileRouter = getFileRouter(db);
257
255
  const avatarRouter = getAvatarRouter();
256
+ const billingRouter = getBillingRouter(db, notify);
257
+ const entitlementRouter = getEntitlementRouter(db, notify);
258
258
  const inviteRouter = getInviteRouter(db, tokenValidator, notify);
259
259
  const passkeyRouter = getPasskeyRouter(db);
260
260
  const passkeyDeviceRouter = getPasskeyDeviceRouter(
@@ -684,17 +684,6 @@ export const initApp = (
684
684
  res.sendStatus(401);
685
685
  return;
686
686
  }
687
- const passkeys = await db.retrievePasskeysByUser(device.owner);
688
- // The CLI stress harness cannot perform a WebAuthn ceremony.
689
- // Keep normal clients gated, but let the existing dev/load-test key
690
- // exercise device connect on disposable accounts.
691
- if (passkeys.length === 0 && !devApiKeyMatches(req)) {
692
- res.status(403).send({
693
- error: "A passkey must be registered before this device can connect.",
694
- });
695
- return;
696
- }
697
-
698
687
  const regKey = await spireXSignOpenAsync(
699
688
  signed,
700
689
  XUtils.decodeHex(device.signKey),
@@ -1024,6 +1013,8 @@ export const initApp = (
1024
1013
  // an authenticated device) and `/auth/passkey/...` (public
1025
1014
  // sign-in). The router itself defines the full path on each
1026
1015
  // route handler.
1016
+ api.use(billingRouter);
1017
+ api.use(entitlementRouter);
1027
1018
  api.use(passkeyRouter);
1028
1019
  api.use(passkeyDeviceRouter);
1029
1020
 
@@ -204,10 +204,9 @@ export const getPasskeyRouter = (db: Database) => {
204
204
  }
205
205
 
206
206
  const existing = await db.retrievePasskeysByUser(userID);
207
- // A freshly registered account has a bearer token but cannot
208
- // get a device token until its first passkey exists. Allow
209
- // exactly that bootstrap case; every later passkey addition
210
- // must come from an authenticated device session.
207
+ // A freshly registered account may add its first passkey with
208
+ // the account bearer before finishing device connect. Every later
209
+ // passkey addition must come from an authenticated device session.
211
210
  if (!req.device && existing.length > 0) {
212
211
  res.status(401).send({
213
212
  error: "Adding another passkey requires an authenticated device.",
@@ -1,13 +0,0 @@
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 { MailWS } from "@vex-chat/types";
7
- export interface CallWakeDispatchData {
8
- callID: string;
9
- expiresAt?: string | undefined;
10
- mailID: string;
11
- mailNonce: string;
12
- }
13
- export declare function callWakeDispatchData(mail: MailWS): CallWakeDispatchData | null;
@@ -1,18 +0,0 @@
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 { XUtils } from "@vex-chat/crypto";
7
- export function callWakeDispatchData(mail) {
8
- if (mail.notify?.event !== "callWake") {
9
- return null;
10
- }
11
- return {
12
- callID: mail.notify.callID,
13
- ...(mail.notify.expiresAt ? { expiresAt: mail.notify.expiresAt } : {}),
14
- mailID: mail.mailID,
15
- mailNonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
16
- };
17
- }
18
- //# sourceMappingURL=callWake.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"callWake.js","sourceRoot":"","sources":["../../src/server/callWake.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAS1C,MAAM,UAAU,oBAAoB,CAChC,IAAY;IAEZ,IAAI,IAAI,CAAC,MAAM,EAAE,KAAK,KAAK,UAAU,EAAE,CAAC;QACpC,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,OAAO;QACH,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;QAC1B,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;KAC1D,CAAC;AACN,CAAC"}