Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add publishable_key_auth #234

Merged
merged 3 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/lib/middleware/with-publishable-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
BadRequestException,
type Middleware,
UnauthorizedException,
} from "nextlove"
import type { AuthenticatedRequest } from "src/types/authenticated-request.ts"

import type { Database } from "lib/database/schema.ts"

export const withPublishableKey: Middleware<
{
auth: Extract<AuthenticatedRequest["auth"], { type: "publishable_key" }>
},
{
db: Database
}
> = (next) => async (req, res) => {
const is_token_auth = req.headers.authorization != null
if (is_token_auth) {
throw new BadRequestException({
type: "token_auth_not_accepted",
message: "Token auth is not accepted for this request.",
})
}

const publishable_key = req.headers["seam-publishable-key"]
if (publishable_key == null) {
throw new UnauthorizedException({
type: "publishable_key_header_required",
message: "Seam-Publishable-Key header required.",
})
}

if (typeof publishable_key !== "string") {
throw new UnauthorizedException({
type: "invalid_publishable_key",
message: "seam-publishable-key must be a string",
})
}

const { user_identifier_key } = req.body

if ((user_identifier_key?.trim() ?? "").length === 0) {
throw new UnauthorizedException({
type: "publishable_key_requires_user_identifier_key",
message: "user_identifier_key is required with publishable_key",
})
}

const workspace = req.db.workspaces.find(
(w) => w.publishable_key === publishable_key,
)

if (workspace == null) {
throw new UnauthorizedException({
type: "workspace_not_found",
message: "Cannot find workspace associated with this publishable_key",
})
}

req.auth = {
type: "publishable_key",
workspace_id: workspace.workspace_id,
publishable_key,
sandbox: workspace.is_sandbox,
}

return next(req, res)
}
7 changes: 7 additions & 0 deletions src/lib/middleware/with-route-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { withBaseUrl } from "./with-base-url.ts"
import { withClientSession } from "./with-client-session.ts"
import { withCors } from "./with-cors.ts"
import { withDb } from "./with-db.ts"
import { withPublishableKey } from "./with-publishable-key.ts"
import { withRequestId } from "./with-request-id.ts"
import { withSessionAuth } from "./with-session-auth.ts"

Expand Down Expand Up @@ -47,6 +48,11 @@ export const withRouteSpec = createWithRouteSpec({
scheme: "bearer",
bearerFormat: "API Key",
},
publishable_key: {
type: "apiKey",
in: "header",
name: "seam-publishable-key",
},
},

authMiddlewareMap: {
Expand All @@ -62,5 +68,6 @@ export const withRouteSpec = createWithRouteSpec({
}),
api_key: withApiKey,
client_session: withClientSession,
publishable_key: withPublishableKey,
},
} as const)
114 changes: 22 additions & 92 deletions src/pages/api/client_sessions/create.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,34 @@
import {
BadRequestException,
HttpException,
UnauthorizedException,
} from "nextlove"
import { BadRequestException, HttpException } from "nextlove"
import { z } from "zod"

import { withRouteSpec } from "lib/middleware/index.ts"

import { client_session } from "lib/zod/client_session.ts"

export default withRouteSpec({
auth: "none",
auth: [
"api_key",
"pat_with_workspace",
"console_session_with_workspace",
"publishable_key",
],
methods: ["POST", "PUT"],
middlewares: [],
jsonBody: z.union([
z.any(),
z
.object({
connected_account_ids: z.array(z.string()).optional(),
connect_webview_ids: z.array(z.string()).optional(),
user_identifier_key: z.string().optional(),
})
.optional(),
]),
jsonBody: z.object({
connected_account_ids: z.array(z.string()).optional(),
connect_webview_ids: z.array(z.string()).optional(),
user_identifier_key: z.string().optional(),
}),
jsonResponse: z.object({
client_session,
ok: z.literal(true),
}),
} as const)(async (req, res) => {
const user_identifier_key =
req.body?.user_identifier_key ??
(req.headers["user-identifier-key"] as string | undefined) ??
(req.headers["seam-user-identifier-key"] as string | undefined)

const publishable_key = req.headers["seam-publishable-key"] as
| string
| undefined

const token =
req.headers["seam-api-key"] ??
req.headers.authorization?.split("Bearer ")?.[1]

if (publishable_key == null && token == null) {
throw new BadRequestException({
type: "seam_api_or_publishable_key_header_required",
message: "Seam-Api-Key or Seam-Publishable-Key header required",
})
}

if (publishable_key != null && user_identifier_key == null) {
throw new UnauthorizedException({
type: "missing_user_identifier_key",
message:
"You must provide a user_identifier_key when using a publishable key",
})
}

if (token == null && user_identifier_key == null) {
throw new UnauthorizedException({
type: "missing_user_identifier_key",
message: "You must provide a user_identifier_key when using an api key",
})
}

const { connect_webview_ids, connected_account_ids } = req.body
const { connect_webview_ids, connected_account_ids, user_identifier_key } =
req.body

if (
publishable_key != null &&
req.auth.type === "publishable_key" &&
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
(connect_webview_ids != null || connected_account_ids != null)
) {
Expand All @@ -77,39 +39,11 @@ export default withRouteSpec({
})
}

let workspace_id: string | null = null
let api_key
if (token != null && publishable_key == null) {
api_key = req.db.api_keys.find((a) => a.token === token)

if (api_key == null) {
throw new BadRequestException({
type: "invalid_api_key",
message: "Invalid api key",
})
}
workspace_id = api_key.workspace_id
}

if (publishable_key != null && token == null) {
const workspace = req.db.workspaces.find(
(w) => w.publishable_key === publishable_key,
)

if (workspace == null) {
throw new BadRequestException({
type: "Workspace not found",
message: "Workspace not found",
})
}
workspace_id = workspace.workspace_id
}

if (user_identifier_key != null) {
const existing_cs = req.db.client_sessions.find(
(cst) =>
cst.user_identifier_key === user_identifier_key &&
cst.workspace_id === workspace_id,
cst.workspace_id === req.auth.workspace_id,
)

if (existing_cs != null) {
Expand All @@ -128,20 +62,16 @@ export default withRouteSpec({
}
}

if (workspace_id == null) {
throw new BadRequestException({
type: "workspace_id_not_found",
message: "Workspace id not found",
})
}

const client_session = req.db.addClientSession({
workspace_id,
workspace_id: req.auth.workspace_id,
connect_webview_ids,
connected_account_ids,
user_identifier_key,
publishable_key,
api_key_id: api_key?.api_key_id,
publishable_key:
req.auth.type === "publishable_key"
? req.auth.publishable_key
: undefined,
api_key_id: req.auth.type === "api_key" ? req.auth.api_key_id : undefined,
})
const device_count = req.db.devices.filter(
(d) =>
Expand Down
15 changes: 5 additions & 10 deletions src/route-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1542,16 +1542,11 @@ export type Routes = {
route: "/client_sessions/create"
method: "POST" | "PUT"
queryParams: {}
jsonBody:
| any
| (
| {
connected_account_ids?: string[] | undefined
connect_webview_ids?: string[] | undefined
user_identifier_key?: string | undefined
}
| undefined
)
jsonBody: {
connected_account_ids?: string[] | undefined
connect_webview_ids?: string[] | undefined
user_identifier_key?: string | undefined
}
commonParams: {}
formData: {}
jsonResponse: {
Expand Down
6 changes: 6 additions & 0 deletions src/types/authenticated-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,10 @@ export type AuthenticatedRequest = Request & {
user_id: string
user_session_id: string
}
| {
type: "publishable_key"
publishable_key: string
workspace_id: string
sandbox: boolean
}
}
Loading