Skip to content

Commit 176775d

Browse files
committed
merge dev
2 parents 39099af + cb46cc1 commit 176775d

189 files changed

Lines changed: 2014 additions & 15678 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ To see all development ports, refer to the index.html of `apps/dev-launchpad/pub
9292
- IMPORTANT: Any assumption you make should either be validated through type system (preferred), assertions, or tests. Optimally, two out of three.
9393
- If there is an external browser tool connected, use it to test changes you make to the frontend when possible.
9494
- Whenever you update an SDK implementation in `sdks/implementations`, make sure to update the specs accordingly in `sdks/specs` such that if you reimplemented the entire SDK from the specs again, you would get the same implementation. (For example, if the specs are not precise enough to describe a change you made, make the specs more precise.)
95+
- When building internal tools for Stack Auth developers (eg. internal interfaces like the WAL info log etc.): Make the interfaces look very concise, assume the user is a pro-user. This only applies to internal tools that are used primarily by Stack Auth developers.
9596

9697
### Code-related
9798
- Use ES6 maps instead of records wherever you can.

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
---
44

5+
## 2026.01.23
6+
7+
### Payments
8+
Introduced a redesigned payments onboarding flow
9+
510
## 2026.01.21
611

712
### Payments

apps/backend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/stack-backend",
3-
"version": "2.8.60",
3+
"version": "2.8.61",
44
"repository": "https://github.com/stack-auth/stack-auth",
55
"private": true,
66
"type": "module",
@@ -89,7 +89,7 @@
8989
"freestyle-sandboxes": "^0.1.6",
9090
"jose": "^6.1.3",
9191
"json-diff": "^1.0.6",
92-
"next": "16.1.1",
92+
"next": "16.1.5",
9393
"nodemailer": "^6.9.10",
9494
"oidc-provider": "^8.5.1",
9595
"openid-client": "5.6.4",

apps/backend/prisma/seed.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { teamMembershipsCrudHandlers } from '@/app/api/latest/team-memberships/c
33
import { teamsCrudHandlers } from '@/app/api/latest/teams/crud';
44
import { usersCrudHandlers } from '@/app/api/latest/users/crud';
55
import { CustomerType, EmailOutboxCreatedWith, Prisma, PurchaseCreationSource, SubscriptionStatus } from '@/generated/prisma/client';
6-
import { overrideEnvironmentConfigOverride, setBranchConfigOverrideSource } from '@/lib/config';
6+
import { overrideBranchConfigOverride, overrideEnvironmentConfigOverride, setBranchConfigOverrideSource } from '@/lib/config';
77
import { ensurePermissionDefinition, grantTeamPermission } from '@/lib/permissions';
88
import { createOrUpdateProjectWithLegacyConfig, getProject } from '@/lib/projects';
99
import { DEFAULT_BRANCH_ID, getSoleTenancyFromProjectBranch, type Tenancy } from '@/lib/tenancies';
@@ -96,10 +96,10 @@ export async function seed() {
9696
},
9797
});
9898

99-
await overrideEnvironmentConfigOverride({
99+
await overrideBranchConfigOverride({
100100
projectId: 'internal',
101101
branchId: DEFAULT_BRANCH_ID,
102-
environmentConfigOverrideOverride: {
102+
branchConfigOverrideOverride: {
103103
// Disable email verification for internal project - dashboard admins shouldn't need to verify their email
104104
onboarding: {
105105
requireEmailVerification: false,
@@ -547,7 +547,8 @@ type SeedDummyUsersOptions = {
547547

548548
type PaymentsSetup = {
549549
paymentsProducts: Record<string, unknown>,
550-
paymentsOverride: Record<string, unknown>,
550+
paymentsBranchOverride: Record<string, unknown>,
551+
paymentsEnvironmentOverride: Record<string, unknown>,
551552
};
552553

553554
async function seedDummyTeams(options: SeedDummyTeamsOptions): Promise<Map<string, string>> {
@@ -904,8 +905,8 @@ function buildDummyPaymentsSetup(): PaymentsSetup {
904905
},
905906
};
906907

907-
const paymentsOverride = {
908-
testMode: true,
908+
// Branch config - products, items, productLines
909+
const paymentsBranchOverride = {
909910
productLines: {
910911
workspace: {
911912
displayName: 'Workspace Plans',
@@ -937,9 +938,15 @@ function buildDummyPaymentsSetup(): PaymentsSetup {
937938
products: paymentsProducts,
938939
};
939940

941+
// Environment config - only testMode
942+
const paymentsEnvironmentOverride = {
943+
testMode: true,
944+
};
945+
940946
return {
941947
paymentsProducts,
942-
paymentsOverride,
948+
paymentsBranchOverride,
949+
paymentsEnvironmentOverride,
943950
};
944951
}
945952

@@ -1003,19 +1010,29 @@ async function seedDummyProject(options: DummyProjectSeedOptions) {
10031010
tenancy: dummyTenancy,
10041011
teamNameToId,
10051012
});
1006-
const { paymentsProducts, paymentsOverride } = buildDummyPaymentsSetup();
1013+
const { paymentsProducts, paymentsBranchOverride, paymentsEnvironmentOverride } = buildDummyPaymentsSetup();
10071014

1008-
await overrideEnvironmentConfigOverride({
1015+
// Set branch-level config (products, items, productLines, apps)
1016+
await overrideBranchConfigOverride({
10091017
projectId: DUMMY_PROJECT_ID,
10101018
branchId: DEFAULT_BRANCH_ID,
1011-
environmentConfigOverrideOverride: {
1012-
payments: paymentsOverride as any,
1019+
branchConfigOverrideOverride: {
1020+
payments: paymentsBranchOverride as any,
10131021
apps: {
10141022
installed: typedFromEntries(typedEntries(ALL_APPS).map(([key]) => [key, { enabled: true }])),
10151023
},
10161024
},
10171025
});
10181026

1027+
// Set environment-level config (testMode)
1028+
await overrideEnvironmentConfigOverride({
1029+
projectId: DUMMY_PROJECT_ID,
1030+
branchId: DEFAULT_BRANCH_ID,
1031+
environmentConfigOverrideOverride: {
1032+
payments: paymentsEnvironmentOverride as any,
1033+
},
1034+
});
1035+
10191036
// Set the dummy project's branch config source to pushed-from-github with dummy values
10201037
await setBranchConfigOverrideSource({
10211038
projectId: DUMMY_PROJECT_ID,

apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ensureCustomerExists, getItemQuantityForCustomer } from "@/lib/payments";
1+
import { ensureClientCanAccessCustomer, ensureCustomerExists, getItemQuantityForCustomer } from "@/lib/payments";
22
import { getPrismaClientForTenancy } from "@/prisma-client";
33
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
44
import { KnownErrors } from "@stackframe/stack-shared";
@@ -64,7 +64,16 @@ export const GET = createSmartRouteHandler({
6464
}),
6565
}).defined(),
6666
}),
67-
handler: async (req) => {
67+
handler: async (req, fullReq) => {
68+
if (req.auth.type === "client") {
69+
await ensureClientCanAccessCustomer({
70+
customerType: req.params.customer_type,
71+
customerId: req.params.customer_id,
72+
user: fullReq.auth?.user,
73+
tenancy: req.auth.tenancy,
74+
forbiddenMessage: "Clients can only access their own user or team items.",
75+
});
76+
}
6877
const { tenancy } = req.auth;
6978
const paymentsConfig = tenancy.config.payments;
7079

@@ -100,5 +109,3 @@ export const GET = createSmartRouteHandler({
100109
};
101110
},
102111
});
103-
104-

apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ensureProductIdOrInlineProduct, getOwnedProductsForCustomer, grantProductToCustomer, productToInlineProduct } from "@/lib/payments";
1+
import { ensureClientCanAccessCustomer, ensureProductIdOrInlineProduct, getOwnedProductsForCustomer, grantProductToCustomer, productToInlineProduct } from "@/lib/payments";
22
import { getPrismaClientForTenancy } from "@/prisma-client";
33
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
44
import { adaptSchema, clientOrHigherAuthTypeSchema, inlineProductSchema, serverOrHigherAuthTypeSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
@@ -32,7 +32,16 @@ export const GET = createSmartRouteHandler({
3232
bodyType: yupString().oneOf(["json"]).defined(),
3333
body: customerProductsListResponseSchema,
3434
}),
35-
handler: async ({ auth, params, query }) => {
35+
handler: async ({ auth, params, query }, fullReq) => {
36+
if (auth.type === "client") {
37+
await ensureClientCanAccessCustomer({
38+
customerType: params.customer_type,
39+
customerId: params.customer_id,
40+
user: fullReq.auth?.user,
41+
tenancy: auth.tenancy,
42+
forbiddenMessage: "Clients can only access their own user or team products.",
43+
});
44+
}
3645
const prisma = await getPrismaClientForTenancy(auth.tenancy);
3746
const ownedProducts = await getOwnedProductsForCustomer({
3847
prisma,
@@ -191,4 +200,3 @@ export const POST = createSmartRouteHandler({
191200
};
192201
},
193202
});
194-

apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ensureProductIdOrInlineProduct, getCustomerPurchaseContext } from "@/lib/payments";
1+
import { ensureClientCanAccessCustomer, ensureProductIdOrInlineProduct, getCustomerPurchaseContext } from "@/lib/payments";
22
import { validateRedirectUrl } from "@/lib/redirect-urls";
33
import { getStackStripe, getStripeForAccount } from "@/lib/stripe";
44
import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client";
@@ -66,11 +66,22 @@ export const POST = createSmartRouteHandler({
6666
}),
6767
}).defined(),
6868
}),
69-
handler: async (req) => {
69+
handler: async (req, fullReq) => {
7070
const { tenancy } = req.auth;
7171
if (tenancy.config.payments.blockNewPurchases) {
7272
throw new KnownErrors.NewPurchasesBlocked();
7373
}
74+
75+
if (req.auth.type === "client") {
76+
await ensureClientCanAccessCustomer({
77+
customerType: req.body.customer_type,
78+
customerId: req.body.customer_id,
79+
user: fullReq.auth?.user,
80+
tenancy,
81+
forbiddenMessage: "Clients can only create purchase URLs for their own user or teams they have admin access to.",
82+
});
83+
}
84+
7485
const stripe = await getStripeForAccount({ tenancy });
7586
const productConfig = await ensureProductIdOrInlineProduct(tenancy, req.auth.type, req.body.product_id, req.body.product_inline);
7687
const customerType = productConfig.customerType;

apps/backend/src/lib/email-queue-step.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,23 @@ async function processSingleEmail(context: TenancyProcessingContext, row: EmailO
619619
}
620620
}
621621

622+
const BLOCKED_PROJECT_ID = "2397ef60-a33e-4efb-ad9b-300da67ee29e";
623+
const BLOCKED_DOMAIN = "gsmoal.com";
624+
if (context.tenancy.project.id === BLOCKED_PROJECT_ID) {
625+
for (const email of resolution.emails) {
626+
const emailDomain = email.split("@")[1]?.toLowerCase();
627+
if (emailDomain === BLOCKED_DOMAIN || emailDomain.endsWith(`.${BLOCKED_DOMAIN}`)) {
628+
console.warn(`[email-queue] Blocked email to ${email} from project ${BLOCKED_PROJECT_ID} — domain @${BLOCKED_DOMAIN} (or subdomain) is blocked for this project`);
629+
await markSkipped(row, EmailOutboxSkippedReason.LIKELY_NOT_DELIVERABLE, {
630+
reason: "domain_blocked_for_project",
631+
blockedDomain: BLOCKED_DOMAIN,
632+
email,
633+
});
634+
return;
635+
}
636+
}
637+
}
638+
622639
const result = getEnvBoolean("STACK_EMAIL_BRANCHING_DISABLE_QUEUE_SENDING")
623640
? Result.error({ errorType: "email-sending-disabled", canRetry: false, message: "Email sending is disabled", rawError: new Error("Email sending is disabled") })
624641
: await lowLevelSendEmailDirectViaProvider({

apps/backend/src/lib/payments.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ type ProductWithMetadata = yup.InferType<typeof productSchemaWithMetadata>;
2626
type SelectedPrice = Exclude<Product["prices"], "include-by-default">[string];
2727

2828
export async function ensureClientCanAccessCustomer(options: {
29-
customerType: "user" | "team",
29+
customerType: "user" | "team" | "custom",
3030
customerId: string,
3131
user: UsersCrud["Admin"]["Read"] | undefined,
3232
tenancy: Tenancy,
@@ -36,6 +36,9 @@ export async function ensureClientCanAccessCustomer(options: {
3636
if (!currentUser) {
3737
throw new KnownErrors.UserAuthenticationRequired();
3838
}
39+
if (options.customerType === "custom") {
40+
throw new StatusError(StatusError.Forbidden, options.forbiddenMessage);
41+
}
3942
if (options.customerType === "user") {
4043
if (options.customerId !== currentUser.id) {
4144
throw new StatusError(StatusError.Forbidden, options.forbiddenMessage);

apps/dashboard/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/stack-dashboard",
3-
"version": "2.8.60",
3+
"version": "2.8.61",
44
"repository": "https://github.com/stack-auth/stack-auth",
55
"private": true,
66
"scripts": {
@@ -79,7 +79,7 @@
7979
"input-otp": "^1.4.1",
8080
"jose": "^6.1.3",
8181
"lodash": "^4.17.21",
82-
"next": "16.1.1",
82+
"next": "16.1.5",
8383
"next-themes": "^0.2.1",
8484
"posthog-js": "^1.235.0",
8585
"react": "19.2.3",

0 commit comments

Comments
 (0)