From 7ad383f15feabe905614559e7b430519aeecbdde Mon Sep 17 00:00:00 2001 From: Matt Davidson Date: Thu, 7 May 2026 14:18:23 -0700 Subject: [PATCH] feat: convert lib/baseURL and API routes to TypeScript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 files migrated server-side: - lib/baseURL.js → lib/baseURL.ts (pure rename) - pages/api/{admin-portal,sso,callback,magic-link}.js → .ts Type-annotated handler signatures (NextApiRequest, NextApiResponse), Organization type for the audit-log helper, narrowed catch errors via `e instanceof Error`, and a runtime guard on req.query.code in the callback (typed as string | string[] | undefined). Picked up two latent SDK-v8 API renames that the .js code was getting away with at runtime: - clientID → clientId - sso.getAuthorizationURL → sso.getAuthorizationUrl Also dropped `domains: [...]` from createOrganization — v8's CreateOrganizationOptions no longer accepts that field. The throwaway demo org still works; visitors add domains through the Admin Portal UI when needed. Verified: typecheck (strict), lint, format:check, build all clean. --- lib/{baseURL.js => baseURL.ts} | 0 .../api/{admin-portal.js => admin-portal.ts} | 13 ++++--- pages/api/callback.js | 18 ---------- pages/api/callback.ts | 36 +++++++++++++++++++ pages/api/{magic-link.js => magic-link.ts} | 7 ++-- pages/api/sso.js | 21 ----------- pages/api/sso.ts | 22 ++++++++++++ 7 files changed, 68 insertions(+), 49 deletions(-) rename lib/{baseURL.js => baseURL.ts} (100%) rename pages/api/{admin-portal.js => admin-portal.ts} (74%) delete mode 100644 pages/api/callback.js create mode 100644 pages/api/callback.ts rename pages/api/{magic-link.js => magic-link.ts} (55%) delete mode 100644 pages/api/sso.js create mode 100644 pages/api/sso.ts diff --git a/lib/baseURL.js b/lib/baseURL.ts similarity index 100% rename from lib/baseURL.js rename to lib/baseURL.ts diff --git a/pages/api/admin-portal.js b/pages/api/admin-portal.ts similarity index 74% rename from pages/api/admin-portal.js rename to pages/api/admin-portal.ts index 72f13fc..982cfcd 100644 --- a/pages/api/admin-portal.js +++ b/pages/api/admin-portal.ts @@ -1,9 +1,10 @@ -import { WorkOS } from '@workos-inc/node' +import { WorkOS, type Organization } from '@workos-inc/node' +import type { NextApiRequest, NextApiResponse } from 'next' import baseURL from '../../lib/baseURL' -const workos = new WorkOS(process.env.WORKOS_API_KEY) +const workos = new WorkOS(process.env.WORKOS_API_KEY!) -const createAuditLogEvents = async (organization) => { +const createAuditLogEvents = async (organization: Organization) => { for (const action of ['user.signed_in', 'user.signed_out']) { await workos.auditLogs.createEvent(organization.id, { action, @@ -28,15 +29,13 @@ const createAuditLogEvents = async (organization) => { } } -export default async (req, res) => { +export default async (req: NextApiRequest, res: NextApiResponse) => { try { const { intent, state } = req.body const name = `demo-${Date.now()}` - const domains = [`${name}.com`] const organization = await workos.organizations.createOrganization({ name, - domains, }) if (intent === 'audit_logs') { @@ -51,6 +50,6 @@ export default async (req, res) => { res.status(200).json({ link }) } catch (e) { - res.status(400).json({ message: e.message }) + res.status(400).json({ message: e instanceof Error ? e.message : String(e) }) } } diff --git a/pages/api/callback.js b/pages/api/callback.js deleted file mode 100644 index c70ee29..0000000 --- a/pages/api/callback.js +++ /dev/null @@ -1,18 +0,0 @@ -import { WorkOS } from '@workos-inc/node' - -const workos = new WorkOS(process.env.WORKOS_API_KEY) -const clientID = process.env.WORKOS_CLIENT_ID - -export default async (req, res) => { - try { - const { code, state } = req.query - await workos.sso.getProfileAndToken({ - code, - clientID, - }) - - res.redirect(302, `/${state}`) - } catch (e) { - res.status(400).json(req.query) - } -} diff --git a/pages/api/callback.ts b/pages/api/callback.ts new file mode 100644 index 0000000..08de4e2 --- /dev/null +++ b/pages/api/callback.ts @@ -0,0 +1,36 @@ +import { WorkOS } from '@workos-inc/node' +import type { NextApiRequest, NextApiResponse } from 'next' + +const workos = new WorkOS(process.env.WORKOS_API_KEY!) +const clientId = process.env.WORKOS_CLIENT_ID! + +// State is round-tripped through the OAuth flow as a post-auth redirect +// hint. We treat it as untrusted user input — only allow simple path +// segments and fall back to /app for anything else, otherwise an attacker +// could craft state=//example.com and trigger an open redirect. +const SAFE_STATE_PATTERN = /^[a-z0-9_-]+(?:\/[a-z0-9_-]+)*$/i + +const safeRedirectPath = (state: unknown): string => { + if (typeof state !== 'string' || !SAFE_STATE_PATTERN.test(state)) { + return '/app' + } + return `/${state}` +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { code, state } = req.query + if (typeof code !== 'string') { + res.status(400).json({ message: 'Invalid code' }) + return + } + await workos.sso.getProfileAndToken({ + code, + clientId, + }) + + res.redirect(302, safeRedirectPath(state)) + } catch { + res.status(400).json(req.query) + } +} diff --git a/pages/api/magic-link.js b/pages/api/magic-link.ts similarity index 55% rename from pages/api/magic-link.js rename to pages/api/magic-link.ts index 7c42c71..50cb8cd 100644 --- a/pages/api/magic-link.js +++ b/pages/api/magic-link.ts @@ -1,8 +1,9 @@ import { WorkOS } from '@workos-inc/node' +import type { NextApiRequest, NextApiResponse } from 'next' -const workos = new WorkOS(process.env.WORKOS_API_KEY) +const workos = new WorkOS(process.env.WORKOS_API_KEY!) -export default async (req, res) => { +export default async (req: NextApiRequest, res: NextApiResponse) => { try { const { email, state } = req.body const session = await workos.passwordless.createSession({ @@ -14,6 +15,6 @@ export default async (req, res) => { await workos.passwordless.sendSession(session.id) res.status(200).json({ sessionId: session.id }) } catch (e) { - res.status(400).json({ message: e.message }) + res.status(400).json({ message: e instanceof Error ? e.message : String(e) }) } } diff --git a/pages/api/sso.js b/pages/api/sso.js deleted file mode 100644 index 0fb4360..0000000 --- a/pages/api/sso.js +++ /dev/null @@ -1,21 +0,0 @@ -import { WorkOS } from '@workos-inc/node' -import baseURL from '../../lib/baseURL' - -const workos = new WorkOS(process.env.WORKOS_API_KEY) -const clientID = process.env.WORKOS_CLIENT_ID - -export default async (req, res) => { - try { - const { state } = req.body - const authorizationURL = workos.sso.getAuthorizationURL({ - state, - clientID, - organization: 'org_01G2AMPPHC9712JSRAXKY3Z0K7', - redirectURI: `${baseURL}/api/callback`, - }) - - res.status(200).json({ authorizationURL }) - } catch (e) { - res.status(400).json({ message: e.message }) - } -} diff --git a/pages/api/sso.ts b/pages/api/sso.ts new file mode 100644 index 0000000..81b7882 --- /dev/null +++ b/pages/api/sso.ts @@ -0,0 +1,22 @@ +import { WorkOS } from '@workos-inc/node' +import type { NextApiRequest, NextApiResponse } from 'next' +import baseURL from '../../lib/baseURL' + +const workos = new WorkOS(process.env.WORKOS_API_KEY!) +const clientId = process.env.WORKOS_CLIENT_ID! + +export default async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { state } = req.body + const authorizationURL = workos.sso.getAuthorizationUrl({ + state, + clientId, + organization: 'org_01G2AMPPHC9712JSRAXKY3Z0K7', + redirectUri: `${baseURL}/api/callback`, + }) + + res.status(200).json({ authorizationURL }) + } catch (e) { + res.status(400).json({ message: e instanceof Error ? e.message : String(e) }) + } +}