@nextsparkjs/core 0.1.0-beta.127 → 0.1.0-beta.128

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 (91) hide show
  1. package/dist/components/billing/ManageBillingButton.d.ts +3 -3
  2. package/dist/lib/api/rate-limit.d.ts.map +1 -1
  3. package/dist/lib/api/rate-limit.js +9 -6
  4. package/dist/lib/billing/config-types.d.ts +2 -5
  5. package/dist/lib/billing/config-types.d.ts.map +1 -1
  6. package/dist/lib/billing/gateways/factory.d.ts +13 -2
  7. package/dist/lib/billing/gateways/factory.d.ts.map +1 -1
  8. package/dist/lib/billing/gateways/factory.js +13 -6
  9. package/dist/lib/billing/gateways/interface.d.ts +19 -1
  10. package/dist/lib/billing/gateways/interface.d.ts.map +1 -1
  11. package/dist/lib/billing/gateways/polar.d.ts +8 -1
  12. package/dist/lib/billing/gateways/polar.d.ts.map +1 -1
  13. package/dist/lib/billing/gateways/polar.js +25 -0
  14. package/dist/lib/billing/gateways/stripe.d.ts +8 -26
  15. package/dist/lib/billing/gateways/stripe.d.ts.map +1 -1
  16. package/dist/lib/billing/gateways/stripe.js +41 -44
  17. package/dist/lib/billing/gateways/types.d.ts +11 -0
  18. package/dist/lib/billing/gateways/types.d.ts.map +1 -1
  19. package/dist/lib/billing/jobs.d.ts +1 -1
  20. package/dist/lib/billing/polar-webhook.d.ts +38 -0
  21. package/dist/lib/billing/polar-webhook.d.ts.map +1 -0
  22. package/dist/lib/billing/polar-webhook.js +0 -0
  23. package/dist/lib/billing/schema.d.ts +1 -2
  24. package/dist/lib/billing/schema.d.ts.map +1 -1
  25. package/dist/lib/billing/schema.js +1 -1
  26. package/dist/lib/billing/stripe-webhook.d.ts +48 -0
  27. package/dist/lib/billing/stripe-webhook.d.ts.map +1 -0
  28. package/dist/lib/billing/stripe-webhook.js +316 -0
  29. package/dist/lib/billing/types.d.ts +6 -2
  30. package/dist/lib/billing/types.d.ts.map +1 -1
  31. package/dist/lib/rate-limit-redis.d.ts +2 -2
  32. package/dist/lib/rate-limit-redis.d.ts.map +1 -1
  33. package/dist/lib/rate-limit-redis.js +22 -4
  34. package/dist/lib/selectors/core-selectors.d.ts +2 -2
  35. package/dist/lib/selectors/domains/superadmin.selectors.d.ts +2 -2
  36. package/dist/lib/selectors/domains/superadmin.selectors.js +2 -2
  37. package/dist/lib/selectors/selectors.d.ts +4 -4
  38. package/dist/lib/services/invoice.service.d.ts +3 -3
  39. package/dist/lib/services/invoice.service.js +2 -2
  40. package/dist/lib/services/membership.service.d.ts.map +1 -1
  41. package/dist/lib/services/membership.service.js +29 -0
  42. package/dist/lib/services/plan.service.d.ts +0 -3
  43. package/dist/lib/services/plan.service.d.ts.map +1 -1
  44. package/dist/lib/services/plan.service.js +3 -9
  45. package/dist/lib/services/subscription.service.d.ts +5 -5
  46. package/dist/lib/services/subscription.service.d.ts.map +1 -1
  47. package/dist/lib/services/subscription.service.js +54 -41
  48. package/dist/migrations/001_better_auth_and_functions.sql +5 -11
  49. package/dist/migrations/008_team_members_table.sql +27 -23
  50. package/dist/styles/classes.json +1 -1
  51. package/dist/templates/app/api/auth/[...all]/route.ts +35 -0
  52. package/dist/templates/app/api/health/route.ts +43 -23
  53. package/dist/templates/app/api/internal/user-metadata/route.ts +10 -0
  54. package/dist/templates/app/api/superadmin/subscriptions/route.ts +5 -0
  55. package/dist/templates/app/api/superadmin/teams/[teamId]/route.ts +6 -0
  56. package/dist/templates/app/api/v1/billing/cancel/route.ts +8 -10
  57. package/dist/templates/app/api/v1/billing/change-plan/route.ts +2 -2
  58. package/dist/templates/app/api/v1/billing/checkout/route.ts +6 -8
  59. package/dist/templates/app/api/v1/billing/portal/route.ts +5 -5
  60. package/dist/templates/app/api/v1/billing/presets.ts +1 -1
  61. package/dist/templates/app/api/v1/billing/webhooks/polar/route.ts +83 -6
  62. package/dist/templates/app/api/v1/billing/webhooks/stripe/route.ts +18 -421
  63. package/dist/templates/app/layout.tsx +14 -5
  64. package/dist/templates/app/superadmin/subscriptions/page.tsx +16 -14
  65. package/dist/templates/app/superadmin/teams/[teamId]/page.tsx +18 -15
  66. package/dist/templates/contents/themes/starter/tests/cypress/src/features/SuperadminPOM.ts +2 -2
  67. package/dist/templates/lib/billing/polar-webhook-extensions.ts +23 -0
  68. package/dist/templates/lib/billing/stripe-webhook-extensions.ts +23 -0
  69. package/migrations/001_better_auth_and_functions.sql +5 -11
  70. package/migrations/008_team_members_table.sql +27 -23
  71. package/package.json +10 -2
  72. package/scripts/build/registry/generators/billing-registry.mjs +1 -2
  73. package/templates/app/api/auth/[...all]/route.ts +35 -0
  74. package/templates/app/api/health/route.ts +43 -23
  75. package/templates/app/api/internal/user-metadata/route.ts +10 -0
  76. package/templates/app/api/superadmin/subscriptions/route.ts +5 -0
  77. package/templates/app/api/superadmin/teams/[teamId]/route.ts +6 -0
  78. package/templates/app/api/v1/billing/cancel/route.ts +8 -10
  79. package/templates/app/api/v1/billing/change-plan/route.ts +2 -2
  80. package/templates/app/api/v1/billing/checkout/route.ts +6 -8
  81. package/templates/app/api/v1/billing/portal/route.ts +5 -5
  82. package/templates/app/api/v1/billing/presets.ts +1 -1
  83. package/templates/app/api/v1/billing/webhooks/polar/route.ts +83 -6
  84. package/templates/app/api/v1/billing/webhooks/stripe/route.ts +18 -421
  85. package/templates/app/layout.tsx +14 -5
  86. package/templates/app/superadmin/subscriptions/page.tsx +16 -14
  87. package/templates/app/superadmin/teams/[teamId]/page.tsx +18 -15
  88. package/templates/contents/themes/starter/tests/cypress/src/features/SuperadminPOM.ts +2 -2
  89. package/templates/lib/billing/polar-webhook-extensions.ts +23 -0
  90. package/templates/lib/billing/stripe-webhook-extensions.ts +23 -0
  91. package/tests/jest/__mocks__/@nextsparkjs/registries/billing-registry.ts +7 -8
@@ -3,6 +3,7 @@ import { TeamMemberService } from "./team-member.service.js";
3
3
  import { SubscriptionService } from "./subscription.service.js";
4
4
  import { PermissionService } from "./permission.service.js";
5
5
  import { PlanService } from "./plan.service.js";
6
+ import { BILLING_REGISTRY } from "@nextsparkjs/registries/billing-registry";
6
7
  class TeamMembership {
7
8
  userId;
8
9
  teamId;
@@ -141,6 +142,7 @@ class TeamMembership {
141
142
  * }
142
143
  */
143
144
  canPerformAction(action, options) {
145
+ var _a, _b, _c, _d, _e;
144
146
  if (!this.role) {
145
147
  return {
146
148
  allowed: false,
@@ -173,6 +175,33 @@ class TeamMembership {
173
175
  };
174
176
  }
175
177
  }
178
+ const requiredFeature = (_b = (_a = BILLING_REGISTRY.actionMappings) == null ? void 0 : _a.features) == null ? void 0 : _b[action];
179
+ if (requiredFeature && !this.hasFeature(requiredFeature)) {
180
+ return {
181
+ allowed: false,
182
+ reason: "feature_not_in_plan",
183
+ message: `Your plan does not include the required feature: ${requiredFeature}`,
184
+ meta: {
185
+ requiredFeature,
186
+ currentPlan: (_c = this.subscription) == null ? void 0 : _c.planSlug
187
+ }
188
+ };
189
+ }
190
+ const consumedLimit = (_e = (_d = BILLING_REGISTRY.actionMappings) == null ? void 0 : _d.limits) == null ? void 0 : _e[action];
191
+ if (consumedLimit) {
192
+ const quotaCheck = this.checkQuota(consumedLimit, (options == null ? void 0 : options.incrementQuota) ?? 1);
193
+ if (!quotaCheck.allowed) {
194
+ return {
195
+ allowed: false,
196
+ reason: "quota_exceeded",
197
+ message: `Quota exceeded for ${consumedLimit}`,
198
+ meta: {
199
+ limitSlug: consumedLimit,
200
+ remaining: quotaCheck.remaining
201
+ }
202
+ };
203
+ }
204
+ }
176
205
  return { allowed: true };
177
206
  }
178
207
  }
@@ -161,7 +161,6 @@ export declare class PlanService {
161
161
  static getPrice(slug: string, interval: 'monthly' | 'yearly'): number | null;
162
162
  /**
163
163
  * Get provider price ID for a plan.
164
- * Checks generic providerPriceIds first, falls back to Stripe-specific fields.
165
164
  *
166
165
  * @param slug - Plan slug
167
166
  * @param interval - 'monthly' or 'yearly'
@@ -171,7 +170,5 @@ export declare class PlanService {
171
170
  * const priceId = PlanService.getPriceId('pro', 'monthly')
172
171
  */
173
172
  static getPriceId(slug: string, interval: 'monthly' | 'yearly'): string | null;
174
- /** @deprecated Use getPriceId instead */
175
- static getStripePriceId(slug: string, interval: 'monthly' | 'yearly'): string | null;
176
173
  }
177
174
  //# sourceMappingURL=plan.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plan.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/plan.service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAM7D,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAMD,qBAAa,WAAW;IAKtB;;;;;;;;OAQG;WACU,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAatD;;;;;;;;OAQG;WACU,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAa1D;;;;;;;;;;;;OAYG;WACU,IAAI,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAclE;;;;;;;;OAQG;WACU,UAAU,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAK/C;;;;;;;;OAQG;WACU,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBnD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IASrD;;;;;;;;;OASG;IACH,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAc1C;;;;;;;;;OASG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAStD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM;IAKxD;;;;;;;;;OASG;IACH,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO;IAS7D;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAc3D;;;;;;;;OAQG;IACH,MAAM,CAAC,WAAW,IAAI,MAAM,EAAE;IAI9B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI;IAS5E;;;;;;;;;;OAUG;IACH,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI;IAoB9E,yCAAyC;IACzC,MAAM,CAAC,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI;CAGrF"}
1
+ {"version":3,"file":"plan.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/plan.service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAM7D,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAMD,qBAAa,WAAW;IAKtB;;;;;;;;OAQG;WACU,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAatD;;;;;;;;OAQG;WACU,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAa1D;;;;;;;;;;;;OAYG;WACU,IAAI,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAclE;;;;;;;;OAQG;WACU,UAAU,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAK/C;;;;;;;;OAQG;WACU,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBnD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IASrD;;;;;;;;;OASG;IACH,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAc1C;;;;;;;;;OASG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAStD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM;IAKxD;;;;;;;;;OASG;IACH,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO;IAS7D;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAc3D;;;;;;;;OAQG;IACH,MAAM,CAAC,WAAW,IAAI,MAAM,EAAE;IAI9B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI;IAS5E;;;;;;;;;OASG;IACH,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI;CAc/E"}
@@ -243,7 +243,6 @@ class PlanService {
243
243
  }
244
244
  /**
245
245
  * Get provider price ID for a plan.
246
- * Checks generic providerPriceIds first, falls back to Stripe-specific fields.
247
246
  *
248
247
  * @param slug - Plan slug
249
248
  * @param interval - 'monthly' or 'yearly'
@@ -257,15 +256,10 @@ class PlanService {
257
256
  if (!config) {
258
257
  return null;
259
258
  }
260
- if (config.providerPriceIds) {
261
- const id = interval === "yearly" ? config.providerPriceIds.yearly : config.providerPriceIds.monthly;
262
- if (id) return id;
259
+ if (!config.providerPriceIds) {
260
+ return null;
263
261
  }
264
- return interval === "yearly" ? config.stripePriceIdYearly || null : config.stripePriceIdMonthly || null;
265
- }
266
- /** @deprecated Use getPriceId instead */
267
- static getStripePriceId(slug, interval) {
268
- return this.getPriceId(slug, interval);
262
+ return interval === "yearly" ? config.providerPriceIds.yearly || null : config.providerPriceIds.monthly || null;
269
263
  }
270
264
  }
271
265
  export {
@@ -6,11 +6,11 @@
6
6
  *
7
7
  * @module SubscriptionService
8
8
  */
9
- import type { Subscription, SubscriptionWithPlan, SubscriptionStatus, QuotaInfo, CanPerformActionResult, BillingInterval } from '../billing/types';
9
+ import type { Subscription, SubscriptionWithPlan, SubscriptionStatus, QuotaInfo, CanPerformActionResult, BillingInterval, PaymentProvider } from '../billing/types';
10
10
  export interface CreateSubscriptionOptions {
11
11
  billingInterval?: BillingInterval;
12
12
  trialDays?: number;
13
- paymentProvider?: 'stripe' | 'paddle' | 'lemonsqueezy';
13
+ paymentProvider?: PaymentProvider;
14
14
  externalSubscriptionId?: string;
15
15
  externalCustomerId?: string;
16
16
  }
@@ -41,7 +41,7 @@ export declare class SubscriptionService {
41
41
  * const sub = await SubscriptionService.getActive('team-uuid-123')
42
42
  * console.log(sub?.plan.name) // 'Pro Plan'
43
43
  */
44
- static getActive(teamId: string): Promise<SubscriptionWithPlan | null>;
44
+ static getActive(teamId: string, userId?: string): Promise<SubscriptionWithPlan | null>;
45
45
  /**
46
46
  * Get subscription by team ID (alias for getActive)
47
47
  *
@@ -117,7 +117,7 @@ export declare class SubscriptionService {
117
117
  * console.log('Warnings:', result.downgradeWarnings)
118
118
  * }
119
119
  */
120
- static changePlan(teamId: string, targetPlanSlug: string, billingInterval?: BillingInterval): Promise<ChangePlanResult>;
120
+ static changePlan(teamId: string, targetPlanSlug: string, billingInterval?: BillingInterval, userId?: string): Promise<ChangePlanResult>;
121
121
  /**
122
122
  * Pause subscription
123
123
  *
@@ -180,7 +180,7 @@ export declare class SubscriptionService {
180
180
  offset?: number;
181
181
  }): Promise<SubscriptionWithPlan[]>;
182
182
  /**
183
- * Get subscription by external (Stripe) subscription ID
183
+ * Get subscription by external subscription ID (from payment provider)
184
184
  *
185
185
  * @param externalId - External subscription ID from payment provider
186
186
  * @returns Subscription with plan or null
@@ -1 +1 @@
1
- {"version":3,"file":"subscription.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/subscription.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"subscription.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/subscription.service.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from "../db.js";
1
+ import { queryOneWithRLS, queryWithRLS, mutateWithRLS, getTransactionClient } from "../db.js";
2
2
  import { doAction } from "../plugins/hook-system.js";
3
3
  import { BILLING_REGISTRY } from "@nextsparkjs/registries/billing-registry";
4
4
  import { PlanService } from "./plan.service.js";
@@ -41,7 +41,7 @@ class SubscriptionService {
41
41
  * const sub = await SubscriptionService.getActive('team-uuid-123')
42
42
  * console.log(sub?.plan.name) // 'Pro Plan'
43
43
  */
44
- static async getActive(teamId) {
44
+ static async getActive(teamId, userId) {
45
45
  if (!teamId || teamId.trim() === "") {
46
46
  throw new Error("Team ID is required");
47
47
  }
@@ -57,7 +57,8 @@ class SubscriptionService {
57
57
  ORDER BY s."createdAt" DESC
58
58
  LIMIT 1
59
59
  `,
60
- [teamId]
60
+ [teamId],
61
+ userId
61
62
  );
62
63
  if (!result) return null;
63
64
  return {
@@ -325,14 +326,14 @@ class SubscriptionService {
325
326
  * console.log('Warnings:', result.downgradeWarnings)
326
327
  * }
327
328
  */
328
- static async changePlan(teamId, targetPlanSlug, billingInterval = "monthly") {
329
+ static async changePlan(teamId, targetPlanSlug, billingInterval = "monthly", userId) {
329
330
  if (!teamId || teamId.trim() === "") {
330
331
  return { success: false, error: "Team ID is required" };
331
332
  }
332
333
  if (!targetPlanSlug || targetPlanSlug.trim() === "") {
333
334
  return { success: false, error: "Target plan slug is required" };
334
335
  }
335
- const currentSub = await this.getActive(teamId);
336
+ const currentSub = await this.getActive(teamId, userId);
336
337
  if (!(currentSub == null ? void 0 : currentSub.externalSubscriptionId)) {
337
338
  return { success: false, error: "No active subscription found" };
338
339
  }
@@ -354,46 +355,58 @@ class SubscriptionService {
354
355
  try {
355
356
  await getBillingGateway().updateSubscriptionPlan({
356
357
  subscriptionId: currentSub.externalSubscriptionId,
357
- newPriceId,
358
- prorationBehavior: "create_prorations"
358
+ newPriceId
359
359
  });
360
360
  const targetPlan = await PlanService.getBySlug(targetPlanSlug);
361
361
  if (!targetPlan) {
362
362
  return { success: false, error: "Plan not found in database" };
363
363
  }
364
- await queryWithRLS(
365
- `
366
- UPDATE "subscriptions"
367
- SET
368
- "planId" = $1,
369
- "billingInterval" = $2,
370
- metadata = jsonb_set(
371
- COALESCE(metadata, '{}'::jsonb),
372
- '{previousPlanSlug}',
373
- $3::jsonb
374
- ),
375
- "updatedAt" = NOW()
376
- WHERE "teamId" = $4 AND status IN ('active', 'trialing')
377
- `,
378
- [targetPlan.id, billingInterval, JSON.stringify(currentSub.plan.slug), teamId]
379
- );
380
- await queryWithRLS(
381
- `
382
- INSERT INTO "billing_events" (id, "subscriptionId", type, status, metadata, "createdAt")
383
- VALUES ($1, $2, 'lifecycle', 'succeeded', $3, NOW())
384
- `,
385
- [
386
- crypto.randomUUID(),
387
- currentSub.id,
388
- JSON.stringify({
389
- action: "plan_change",
390
- fromPlan: currentSub.plan.slug,
391
- toPlan: targetPlanSlug,
392
- billingInterval
393
- })
394
- ]
395
- );
396
- const updatedSub = await this.getActive(teamId);
364
+ const tx = await getTransactionClient();
365
+ try {
366
+ await tx.query(
367
+ `
368
+ UPDATE "subscriptions"
369
+ SET
370
+ "planId" = $1,
371
+ "billingInterval" = $2,
372
+ metadata = jsonb_set(
373
+ COALESCE(metadata, '{}'::jsonb),
374
+ '{previousPlanSlug}',
375
+ $3::jsonb
376
+ ),
377
+ "updatedAt" = NOW()
378
+ WHERE "teamId" = $4 AND status IN ('active', 'trialing')
379
+ `,
380
+ [targetPlan.id, billingInterval, JSON.stringify(currentSub.plan.slug), teamId]
381
+ );
382
+ await tx.query(
383
+ `
384
+ INSERT INTO "billing_events" (id, "subscriptionId", type, status, amount, currency, metadata, "createdAt")
385
+ VALUES ($1, $2, 'lifecycle', 'succeeded', $3, $4, $5, NOW())
386
+ `,
387
+ [
388
+ crypto.randomUUID(),
389
+ currentSub.id,
390
+ 0,
391
+ "usd",
392
+ JSON.stringify({
393
+ action: "plan_change",
394
+ fromPlan: currentSub.plan.slug,
395
+ toPlan: targetPlanSlug,
396
+ billingInterval
397
+ })
398
+ ]
399
+ );
400
+ await tx.commit();
401
+ } catch (dbError) {
402
+ await tx.rollback();
403
+ console.error("[SubscriptionService.changePlan] DB transaction failed after provider update:", dbError);
404
+ return {
405
+ success: false,
406
+ error: "Plan changed in payment provider but local DB update failed. Please contact support."
407
+ };
408
+ }
409
+ const updatedSub = await this.getActive(teamId, userId);
397
410
  await doAction("subscription.updated", {
398
411
  id: currentSub.id,
399
412
  data: updatedSub,
@@ -544,7 +557,7 @@ class SubscriptionService {
544
557
  return results.map((r) => ({ ...r, plan: r.plan }));
545
558
  }
546
559
  /**
547
- * Get subscription by external (Stripe) subscription ID
560
+ * Get subscription by external subscription ID (from payment provider)
548
561
  *
549
562
  * @param externalId - External subscription ID from payment provider
550
563
  * @returns Subscription with plan or null
@@ -41,17 +41,11 @@ BEGIN
41
41
  END;
42
42
  $$;
43
43
 
44
- -- Alias sin schema si lo usás en policies legadas
45
- CREATE OR REPLACE FUNCTION get_auth_user_id()
46
- RETURNS TEXT
47
- LANGUAGE plpgsql
48
- SECURITY DEFINER
49
- SET search_path = public
50
- AS $$
51
- BEGIN
52
- RETURN public.get_auth_user_id();
53
- END;
54
- $$;
44
+ -- NOTE: Previously there was an alias function `get_auth_user_id()` (without schema)
45
+ -- that called `public.get_auth_user_id()`. This was removed because PostgreSQL's
46
+ -- `CREATE OR REPLACE` with `SET search_path = public` would overwrite the real
47
+ -- function with the alias, causing infinite recursion. All RLS policies should
48
+ -- use `public.get_auth_user_id()` with the explicit schema qualifier.
55
49
 
56
50
  -- Utilidad: updatedAt (si no existe ya en otra migration)
57
51
  CREATE OR REPLACE FUNCTION public.set_updated_at()
@@ -43,6 +43,24 @@ CREATE TRIGGER team_members_set_updated_at
43
43
  BEFORE UPDATE ON public."team_members"
44
44
  FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
45
45
 
46
+ -- ============================================
47
+ -- HELPER FUNCTION (bypasses RLS via SECURITY DEFINER)
48
+ -- ============================================
49
+
50
+ -- Returns team IDs for a given user, bypassing RLS to prevent self-referencing
51
+ -- recursion. Without this, team_members RLS policies query team_members itself,
52
+ -- causing infinite recursion when any other table (subscriptions, billing_events,
53
+ -- usage) references team_members in its own RLS policy chain.
54
+ CREATE OR REPLACE FUNCTION public.get_user_team_ids(p_user_id TEXT)
55
+ RETURNS SETOF TEXT
56
+ LANGUAGE sql
57
+ STABLE
58
+ SECURITY DEFINER
59
+ SET search_path = public
60
+ AS $$
61
+ SELECT "teamId" FROM public."team_members" WHERE "userId" = p_user_id;
62
+ $$;
63
+
46
64
  -- ============================================
47
65
  -- TEAM MEMBERS ROW LEVEL SECURITY (RLS)
48
66
  -- ============================================
@@ -50,23 +68,21 @@ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
50
68
  ALTER TABLE public."team_members" ENABLE ROW LEVEL SECURITY;
51
69
 
52
70
  -- Team members: visible to members of the same team
71
+ -- Uses get_user_team_ids() (SECURITY DEFINER) to avoid self-referencing recursion
53
72
  CREATE POLICY "team_members_select_policy" ON public."team_members"
54
73
  FOR SELECT TO authenticated
55
74
  USING (
56
- "teamId" IN (
57
- SELECT "teamId" FROM public."team_members"
58
- WHERE "userId" = public.get_auth_user_id()
59
- )
75
+ "teamId" IN (SELECT public.get_user_team_ids(public.get_auth_user_id()))
60
76
  );
61
77
 
62
78
  -- Team members: owners and admins can add members
63
79
  CREATE POLICY "team_members_insert_policy" ON public."team_members"
64
80
  FOR INSERT TO authenticated
65
81
  WITH CHECK (
66
- "teamId" IN (
67
- SELECT "teamId" FROM public."team_members"
68
- WHERE "userId" = public.get_auth_user_id()
69
- AND role IN ('owner', 'admin')
82
+ "teamId" IN (SELECT public.get_user_team_ids(public.get_auth_user_id()))
83
+ AND EXISTS (
84
+ SELECT 1 FROM public.get_user_team_ids(public.get_auth_user_id()) AS t
85
+ WHERE t = "team_members"."teamId"
70
86
  )
71
87
  );
72
88
 
@@ -74,29 +90,17 @@ CREATE POLICY "team_members_insert_policy" ON public."team_members"
74
90
  CREATE POLICY "team_members_update_policy" ON public."team_members"
75
91
  FOR UPDATE TO authenticated
76
92
  USING (
77
- "teamId" IN (
78
- SELECT "teamId" FROM public."team_members"
79
- WHERE "userId" = public.get_auth_user_id()
80
- AND role IN ('owner', 'admin')
81
- )
93
+ "teamId" IN (SELECT public.get_user_team_ids(public.get_auth_user_id()))
82
94
  )
83
95
  WITH CHECK (
84
- "teamId" IN (
85
- SELECT "teamId" FROM public."team_members"
86
- WHERE "userId" = public.get_auth_user_id()
87
- AND role IN ('owner', 'admin')
88
- )
96
+ "teamId" IN (SELECT public.get_user_team_ids(public.get_auth_user_id()))
89
97
  );
90
98
 
91
99
  -- Team members: owners and admins can remove members (except themselves)
92
100
  CREATE POLICY "team_members_delete_policy" ON public."team_members"
93
101
  FOR DELETE TO authenticated
94
102
  USING (
95
- "teamId" IN (
96
- SELECT "teamId" FROM public."team_members"
97
- WHERE "userId" = public.get_auth_user_id()
98
- AND role IN ('owner', 'admin')
99
- )
103
+ "teamId" IN (SELECT public.get_user_team_ids(public.get_auth_user_id()))
100
104
  -- Cannot remove yourself
101
105
  AND "userId" != public.get_auth_user_id()
102
106
  );
@@ -1,5 +1,5 @@
1
1
  {
2
- "generated": "2026-03-20T15:42:25.079Z",
2
+ "generated": "2026-03-21T22:28:39.842Z",
3
3
  "totalClasses": 1074,
4
4
  "classes": [
5
5
  "!text-2xl",
@@ -7,6 +7,7 @@ import { isPublicSignupRestricted } from "@nextsparkjs/core/lib/teams/helpers";
7
7
  // Currently domain validation happens in auth.ts databaseHooks
8
8
  import { TeamService } from "@nextsparkjs/core/lib/services";
9
9
  import { wrapAuthHandlerWithCors, handleCorsPreflightRequest, addCorsHeaders } from "@nextsparkjs/core/lib/api/helpers";
10
+ import { checkDistributedRateLimit } from "@nextsparkjs/core/lib/api/rate-limit";
10
11
 
11
12
  const handlers = toNextJsHandler(auth);
12
13
 
@@ -45,6 +46,40 @@ export async function GET(req: NextRequest, context: { params: Promise<{ all: st
45
46
 
46
47
  // Intercept signup requests to validate registration mode
47
48
  export async function POST(req: NextRequest) {
49
+ // Rate limiting: 5 requests per 15 minutes per IP (tier: auth).
50
+ // Protects login/signup against brute-force and credential stuffing attacks.
51
+ // IP extraction strategy:
52
+ // - Cloudflare: cf-connecting-ip (set by Cloudflare, not spoofable behind CF)
53
+ // - Vercel/trusted proxies: rightmost non-private IP in x-forwarded-for
54
+ // - Fallback: x-real-ip or 'unknown'
55
+ const clientIp = (() => {
56
+ // Cloudflare sets this header and it cannot be spoofed when behind CF
57
+ const cfIp = req.headers.get('cf-connecting-ip')
58
+ if (cfIp) return cfIp
59
+
60
+ // x-forwarded-for: use rightmost entry (last proxy-appended value is most trustworthy)
61
+ const forwardedFor = req.headers.get('x-forwarded-for')
62
+ if (forwardedFor) {
63
+ const ips = forwardedFor.split(',').map(ip => ip.trim()).filter(Boolean)
64
+ if (ips.length > 0) return ips[ips.length - 1]
65
+ }
66
+
67
+ return req.headers.get('x-real-ip') || 'unknown'
68
+ })()
69
+ const rateLimitResult = await checkDistributedRateLimit(`auth:ip:${clientIp}`, 'auth')
70
+ if (!rateLimitResult.allowed) {
71
+ return new NextResponse(JSON.stringify({ error: 'Too many requests' }), {
72
+ status: 429,
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ 'Retry-After': '900',
76
+ 'X-RateLimit-Limit': rateLimitResult.limit.toString(),
77
+ 'X-RateLimit-Remaining': '0',
78
+ 'X-RateLimit-Reset': rateLimitResult.resetTime.toString(),
79
+ },
80
+ })
81
+ }
82
+
48
83
  const pathname = req.nextUrl.pathname;
49
84
 
50
85
  // Determine request type
@@ -1,31 +1,51 @@
1
1
  import { queryWithRLS } from "@nextsparkjs/core/lib/db";
2
2
  import { withRateLimitTier } from "@nextsparkjs/core/lib/api/rate-limit";
3
- import { NextResponse } from "next/server";
3
+ import { isRedisConfigured } from "@nextsparkjs/core/lib/rate-limit-redis";
4
+ import { authenticateRequest } from "@nextsparkjs/core/lib/api/auth/dual-auth";
5
+ import { NextRequest, NextResponse } from "next/server";
6
+
7
+ export const GET = withRateLimitTier(async (request: NextRequest) => {
8
+ let dbStatus: 'connected' | 'disconnected' = 'disconnected';
9
+ let dbError: string | undefined;
4
10
 
5
- export const GET = withRateLimitTier(async () => {
6
11
  try {
7
- // Test database connection
8
12
  await queryWithRLS('SELECT 1');
9
-
10
- return NextResponse.json({
11
- status: 'healthy',
12
- timestamp: new Date().toISOString(),
13
- services: {
14
- database: 'connected',
15
- api: 'operational'
16
- }
17
- });
13
+ dbStatus = 'connected';
18
14
  } catch (error) {
19
- console.error('Health check failed:', error);
20
- return NextResponse.json({
21
- status: 'unhealthy',
22
- timestamp: new Date().toISOString(),
23
- error: 'Database connection failed',
24
- services: {
25
- database: 'disconnected',
26
- api: 'operational'
27
- }
28
- }, { status: 503 });
15
+ dbError = error instanceof Error ? error.message : 'Unknown error';
16
+ console.error('[health] Database connection failed:', error);
29
17
  }
30
- }, 'read');
31
18
 
19
+ const healthy = dbStatus === 'connected';
20
+
21
+ // Public response: only healthy/unhealthy status
22
+ const publicResponse = {
23
+ status: healthy ? 'healthy' : 'unhealthy',
24
+ timestamp: new Date().toISOString(),
25
+ };
26
+
27
+ // Check if caller is authenticated for detailed info
28
+ const authResult = await authenticateRequest(request);
29
+ const isAuthenticated = authResult.success && authResult.user;
30
+
31
+ if (!isAuthenticated) {
32
+ return NextResponse.json(publicResponse, { status: healthy ? 200 : 503 });
33
+ }
34
+
35
+ // Authenticated callers get detailed service status
36
+ const redisConfigured = await isRedisConfigured();
37
+
38
+ if (!redisConfigured) {
39
+ console.error('[health] CRITICAL: Redis not configured — rate limiting is DISABLED. Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in environment variables.');
40
+ }
41
+
42
+ return NextResponse.json({
43
+ ...publicResponse,
44
+ services: {
45
+ database: dbStatus,
46
+ rateLimit: redisConfigured ? 'redis' : 'disabled',
47
+ api: 'operational',
48
+ },
49
+ ...(dbError ? { error: dbError } : {}),
50
+ }, { status: healthy ? 200 : 503 });
51
+ }, 'read');
@@ -1,10 +1,16 @@
1
1
  import { NextRequest, NextResponse } from 'next/server'
2
2
  import { MetaService } from '@nextsparkjs/core/lib/services/meta.service'
3
3
  import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
4
+ import { authenticateRequest } from '@nextsparkjs/core/lib/api/auth/dual-auth'
4
5
 
5
6
  // Endpoint interno para crear metadata default después del signup
6
7
  export const POST = withRateLimitTier(async (req: NextRequest) => {
7
8
  try {
9
+ const authResult = await authenticateRequest(req)
10
+ if (!authResult.success || !authResult.user) {
11
+ return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
12
+ }
13
+
8
14
  const body = await req.json()
9
15
  const { userId, metadata } = body
10
16
 
@@ -12,6 +18,10 @@ export const POST = withRateLimitTier(async (req: NextRequest) => {
12
18
  return NextResponse.json({ error: 'User ID is required' }, { status: 400 })
13
19
  }
14
20
 
21
+ if (userId !== authResult.user.id) {
22
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
23
+ }
24
+
15
25
  if (!metadata || typeof metadata !== 'object') {
16
26
  return NextResponse.json({ error: 'Metadata is required' }, { status: 400 })
17
27
  }
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
2
2
  import { getTypedSession } from '@nextsparkjs/core/lib/auth';
3
3
  import { queryWithRLS } from '@nextsparkjs/core/lib/db';
4
4
  import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit';
5
+ import { getBillingGateway } from '@nextsparkjs/core/lib/billing/gateways/factory';
5
6
 
6
7
  interface SubscriptionResult {
7
8
  id: string;
@@ -23,6 +24,7 @@ interface SubscriptionResult {
23
24
  canceledAt: string | null;
24
25
  cancelAtPeriodEnd: boolean;
25
26
  externalSubscriptionId: string | null;
27
+ paymentProvider: string | null;
26
28
  createdAt: string;
27
29
  }
28
30
 
@@ -181,6 +183,7 @@ export const GET = withRateLimitTier(async (request: NextRequest) => {
181
183
  s."canceledAt",
182
184
  s."cancelAtPeriodEnd",
183
185
  s."externalSubscriptionId",
186
+ s."paymentProvider",
184
187
  s."createdAt"
185
188
  FROM "subscriptions" s
186
189
  LEFT JOIN "teams" t ON s."teamId" = t.id
@@ -253,6 +256,8 @@ export const GET = withRateLimitTier(async (request: NextRequest) => {
253
256
  canceledAt: sub.canceledAt,
254
257
  cancelAtPeriodEnd: sub.cancelAtPeriodEnd,
255
258
  externalSubscriptionId: sub.externalSubscriptionId,
259
+ paymentProvider: sub.paymentProvider,
260
+ providerDashboardUrl: getBillingGateway().getSubscriptionDashboardUrl(sub.externalSubscriptionId),
256
261
  createdAt: sub.createdAt
257
262
  }));
258
263
 
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
2
2
  import { getTypedSession } from '@nextsparkjs/core/lib/auth';
3
3
  import { queryWithRLS } from '@nextsparkjs/core/lib/db';
4
4
  import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit';
5
+ import { getBillingGateway } from '@nextsparkjs/core/lib/billing/gateways/factory';
5
6
 
6
7
  interface TeamResult {
7
8
  id: string;
@@ -37,6 +38,7 @@ interface SubscriptionResult {
37
38
  cancelAtPeriodEnd: boolean;
38
39
  externalSubscriptionId: string | null;
39
40
  externalCustomerId: string | null;
41
+ paymentProvider: string | null;
40
42
  createdAt: string;
41
43
  }
42
44
 
@@ -144,6 +146,7 @@ export const GET = withRateLimitTier(async (
144
146
  s."cancelAtPeriodEnd",
145
147
  s."externalSubscriptionId",
146
148
  s."externalCustomerId",
149
+ s."paymentProvider",
147
150
  s."createdAt"
148
151
  FROM "subscriptions" s
149
152
  LEFT JOIN "plans" p ON s."planId" = p.id
@@ -242,6 +245,9 @@ export const GET = withRateLimitTier(async (
242
245
  cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
243
246
  externalSubscriptionId: subscription.externalSubscriptionId,
244
247
  externalCustomerId: subscription.externalCustomerId,
248
+ paymentProvider: subscription.paymentProvider,
249
+ providerName: getBillingGateway().getProviderName(),
250
+ providerDashboardUrl: getBillingGateway().getSubscriptionDashboardUrl(subscription.externalSubscriptionId),
245
251
  createdAt: subscription.createdAt
246
252
  } : null,
247
253
  billingHistory: billingEventsResult.map((event) => ({