Pricing model - Replace seat-based pricing with org-flat-fee plans: Free (10k execs/mo, 3 members), Free PAYG (10k included + $0.20/1k overage), Team ($49/mo, 250k incl, unlimited members, $0.20/1k overage). Bumped Free included executions from 5k -> 10k for top-of-funnel. - Mark `seats` feature archived; introduce `members` (non-consumable metered) and `domain-verification` (boolean) features. - Update privacy/terms copy from "seat counts" to "membership" / "organization-level" pricing language. - Refresh /billing and /billing/plans UI: trim per-seat add-ons, render Free/Team/Enterprise cards, fix button alignment under conditional badges. Slack/email contact CTA on pricing page - New SlackService wraps Slack Web API: creates `#shared-<email-slug>` channel (retries with random suffix on `name_taken`), posts a hello message, and calls `conversations.inviteShared` with `external_limited`. - POST /api/contact/slack: public endpoint, validates email, verifies a Cloudflare Turnstile token, gates on a 5/min global ratelimit binding, then calls SlackService. Internal SlackError details are logged server-side; the client only sees a generic message. - Pricing page CTA: dialog with Turnstile widget + email/name/note form, prefilled from useAuth() when signed in. Sibling "Email us" mailto: link to rhys@executor.sh. - Wrangler bindings: ratelimit (5/60s), VITE_PUBLIC_TURNSTILE_SITEKEY vars; .env.op gets dev test keys + SLACK_BOT_TOKEN op:// reference.
Showing the changes to apps/cloud/src/auth (1 of 17 files). Show all
Unified · Split
| @@ -10,6 +10,7 @@ import { authorizeOrganization } from "./authorize-organization"; · 9 unmodified lines | |||
| 10 | import { env } from "cloudflare:workers"; | 10 | import { env } from "cloudflare:workers"; |
| 11 | import { WorkOSError } from "./errors"; | 11 | import { WorkOSError } from "./errors"; |
| 12 | import { WorkOSAuth } from "./workos"; | 12 | import { WorkOSAuth } from "./workos"; |
| 13 | import { AutumnService } from "../services/autumn"; | ||
| 13 | 14 | ||
| 14 | const COOKIE_OPTIONS = { | 15 | const COOKIE_OPTIONS = { |
| 15 | path: "/", | 16 | path: "/", |
| @@ -69,10 +70,8 @@ const setResponseCookie = ( · 53 unmodified lines | |||
| 69 | options: typeof RESPONSE_COOKIE_OPTIONS, | 70 | options: typeof RESPONSE_COOKIE_OPTIONS, |
| 70 | ) => HttpServerResponse.setCookieUnsafe(response, name, value, options); | 71 | ) => HttpServerResponse.setCookieUnsafe(response, name, value, options); |
| 71 | 72 | ||
| 72 | const deleteResponseCookie = ( | 73 | const deleteResponseCookie = (response: HttpServerResponse.HttpServerResponse, name: string) => |
| 73 | response: HttpServerResponse.HttpServerResponse, | 74 | HttpServerResponse.setCookieUnsafe(response, name, "", DELETE_COOKIE_OPTIONS); |
| 74 | name: string, | ||
| 75 | ) => HttpServerResponse.setCookieUnsafe(response, name, "", DELETE_COOKIE_OPTIONS); | ||
| 76 | 75 | ||
| 77 | // --------------------------------------------------------------------------- | 76 | // --------------------------------------------------------------------------- |
| 78 | // Single non-protected API surface — public (login/callback) + session | 77 | // Single non-protected API surface — public (login/callback) + session |
| @@ -208,7 +207,9 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 129 unmodified lines | |||
| 208 | ); | 207 | ); |
| 209 | 208 | ||
| 210 | return { | 209 | return { |
| 211 | organizations: organizations.filter((org): org is NonNullable<typeof org> => org !== null), | 210 | organizations: organizations.filter( |
| 211 | (org): org is NonNullable<typeof org> => org !== null, | ||
| 212 | ), | ||
| 212 | activeOrganizationId: session.organizationId, | 213 | activeOrganizationId: session.organizationId, |
| 213 | }; | 214 | }; |
| 214 | }), | 215 | }), |
| @@ -231,11 +232,13 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 16 unmodified lines | |||
| 231 | Effect.gen(function* () { | 232 | Effect.gen(function* () { |
| 232 | const workos = yield* WorkOSAuth; | 233 | const workos = yield* WorkOSAuth; |
| 233 | const users = yield* UserStoreService; | 234 | const users = yield* UserStoreService; |
| 235 | const autumn = yield* AutumnService; | ||
| 234 | const session = yield* SessionContext; | 236 | const session = yield* SessionContext; |
| 235 | 237 | ||
| 236 | const name = payload.name.trim(); | 238 | const name = payload.name.trim(); |
| 237 | const org = yield* workos.createOrganization(name); | 239 | const org = yield* workos.createOrganization(name); |
| 238 | yield* workos.createMembership(org.id, session.accountId, "admin"); | 240 | yield* workos.createMembership(org.id, session.accountId, "admin"); |
| 241 | yield* autumn.trackMemberChange(org.id, 1); | ||
| 239 | yield* users.use((s) => s.upsertOrganization({ id: org.id, name: org.name })); | 242 | yield* users.use((s) => s.upsertOrganization({ id: org.id, name: org.name })); |
| 240 | 243 | ||
| 241 | // Try to attach the new org to the current session. This can fail | 244 | // Try to attach the new org to the current session. This can fail |
| @@ -246,9 +249,7 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 4 unmodified lines | |||
| 246 | // cookie and fail loudly; the frontend will bounce to login and | 249 | // cookie and fail loudly; the frontend will bounce to login and |
| 247 | // the callback's rehydrate path will pick up the new membership. | 250 | // the callback's rehydrate path will pick up the new membership. |
| 248 | const refreshed = yield* workos.refreshSession(session.sealedSession, org.id); | 251 | const refreshed = yield* workos.refreshSession(session.sealedSession, org.id); |
| 249 | const verified = refreshed | 252 | const verified = refreshed ? yield* workos.authenticateSealedSession(refreshed) : null; |
| 250 | ? yield* workos.authenticateSealedSession(refreshed) | ||
| 251 | : null; | ||
| 252 | 253 | ||
| 253 | if (!refreshed || !verified || verified.organizationId !== org.id) { | 254 | if (!refreshed || !verified || verified.organizationId !== org.id) { |
| 254 | yield* Effect.logWarning( | 255 | yield* Effect.logWarning( |