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 | 10 | import { env } from "cloudflare:workers"; |
| 11 | 11 | import { WorkOSError } from "./errors"; |
| 12 | 12 | import { WorkOSAuth } from "./workos"; |
| 13 | +import { AutumnService } from "../services/autumn"; | |
| 13 | 14 | |
| 14 | 15 | const COOKIE_OPTIONS = { |
| 15 | 16 | path: "/", |
| @@ -69,10 +70,8 @@ const setResponseCookie = ( · 53 unmodified lines | ||
| 69 | 70 | options: typeof RESPONSE_COOKIE_OPTIONS, |
| 70 | 71 | ) => HttpServerResponse.setCookieUnsafe(response, name, value, options); |
| 71 | 72 | |
| 72 | −const deleteResponseCookie = ( | |
| 73 | − response: HttpServerResponse.HttpServerResponse, | |
| 74 | − name: string, | |
| 75 | −) => HttpServerResponse.setCookieUnsafe(response, name, "", DELETE_COOKIE_OPTIONS); | |
| 73 | +const deleteResponseCookie = (response: HttpServerResponse.HttpServerResponse, name: string) => | |
| 74 | + HttpServerResponse.setCookieUnsafe(response, name, "", DELETE_COOKIE_OPTIONS); | |
| 76 | 75 | |
| 77 | 76 | // --------------------------------------------------------------------------- |
| 78 | 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 | 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 | 213 | activeOrganizationId: session.organizationId, |
| 213 | 214 | }; |
| 214 | 215 | }), |
| @@ -231,11 +232,13 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 16 unmodified lines | ||
| 231 | 232 | Effect.gen(function* () { |
| 232 | 233 | const workos = yield* WorkOSAuth; |
| 233 | 234 | const users = yield* UserStoreService; |
| 235 | + const autumn = yield* AutumnService; | |
| 234 | 236 | const session = yield* SessionContext; |
| 235 | 237 | |
| 236 | 238 | const name = payload.name.trim(); |
| 237 | 239 | const org = yield* workos.createOrganization(name); |
| 238 | 240 | yield* workos.createMembership(org.id, session.accountId, "admin"); |
| 241 | + yield* autumn.trackMemberChange(org.id, 1); | |
| 239 | 242 | yield* users.use((s) => s.upsertOrganization({ id: org.id, name: org.name })); |
| 240 | 243 | |
| 241 | 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 | 249 | // cookie and fail loudly; the frontend will bounce to login and |
| 247 | 250 | // the callback's rehydrate path will pick up the new membership. |
| 248 | 251 | const refreshed = yield* workos.refreshSession(session.sealedSession, org.id); |
| 249 | − const verified = refreshed | |
| 250 | − ? yield* workos.authenticateSealedSession(refreshed) | |
| 251 | − : null; | |
| 252 | + const verified = refreshed ? yield* workos.authenticateSealedSession(refreshed) : null; | |
| 252 | 253 | |
| 253 | 254 | if (!refreshed || !verified || verified.organizationId !== org.id) { |
| 254 | 255 | yield* Effect.logWarning( |