Skip to content

Commit 37cf682

Browse files
authored
Merge pull request #12 from seamapi/fromPublishableKey
Add SeamHttp.fromPublishableKey
2 parents 0669da0 + 4c122cc commit 37cf682

31 files changed

+1051
-401
lines changed

generate-routes.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -233,22 +233,37 @@ ${renderExports(route)}
233233

234234
const renderImports = ({ namespace, subresources }: Route): string =>
235235
`
236-
import type { RouteRequestParams, RouteResponse, RouteRequestBody } from '@seamapi/types/connect'
237-
import { Axios } from 'axios'
236+
import type {
237+
RouteRequestBody,
238+
RouteRequestParams,
239+
RouteResponse,
240+
} from '@seamapi/types/connect'
238241
import type { SetNonNullable } from 'type-fest'
239242
240-
import { createClient } from 'lib/seam/connect/client.js'
243+
import { warnOnInsecureuserIdentifierKey } from 'lib/seam/connect/auth.js'
244+
import {
245+
type Client,
246+
type ClientOptions,
247+
createClient,
248+
} from 'lib/seam/connect/client.js'
241249
import {
242250
isSeamHttpOptionsWithApiKey,
243251
isSeamHttpOptionsWithClient,
244252
isSeamHttpOptionsWithClientSessionToken,
253+
type SeamHttpFromPublishableKeyOptions,
245254
SeamHttpInvalidOptionsError,
246255
type SeamHttpOptions,
247256
type SeamHttpOptionsWithApiKey,
248257
type SeamHttpOptionsWithClient,
249258
type SeamHttpOptionsWithClientSessionToken,
250259
} from 'lib/seam/connect/options.js'
251260
import { parseOptions } from 'lib/seam/connect/parse-options.js'
261+
262+
${
263+
namespace === 'client_sessions'
264+
? ''
265+
: "import { SeamHttpClientSessions } from './client-sessions.js'"
266+
}
252267
${subresources
253268
.map((subresource) => renderSubresourceImport(subresource, namespace))
254269
.join('\n')}
@@ -268,11 +283,15 @@ const renderClass = (
268283
): string =>
269284
`
270285
export class SeamHttp${pascalCase(namespace)} {
271-
client: Axios
286+
client: Client
272287
273288
${constructors
274-
.replace(/.*this\.#legacy.*\n/, '')
275289
.replaceAll(': SeamHttp ', `: SeamHttp${pascalCase(namespace)} `)
290+
.replaceAll('<SeamHttp>', `<SeamHttp${pascalCase(namespace)}>`)
291+
.replaceAll(
292+
'SeamHttp.fromClientSessionToken',
293+
`SeamHttp${pascalCase(namespace)}.fromClientSessionToken`,
294+
)
276295
.replaceAll('new SeamHttp(', `new SeamHttp${pascalCase(namespace)}(`)}
277296
278297
${subresources

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@
8888
"axios-retry": "^3.8.0"
8989
},
9090
"devDependencies": {
91-
"@seamapi/fake-seam-connect": "^1.18.0",
92-
"@seamapi/types": "^1.14.0",
91+
"@seamapi/fake-seam-connect": "^1.21.0",
92+
"@seamapi/types": "^1.24.0",
9393
"@types/eslint": "^8.44.2",
9494
"@types/node": "^18.11.18",
9595
"ava": "^5.0.1",

src/lib/seam/connect/auth.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ import {
22
isSeamHttpOptionsWithApiKey,
33
isSeamHttpOptionsWithClientSessionToken,
44
SeamHttpInvalidOptionsError,
5-
type SeamHttpOptions,
65
type SeamHttpOptionsWithApiKey,
76
type SeamHttpOptionsWithClientSessionToken,
87
} from './options.js'
8+
import type { Options } from './parse-options.js'
99

1010
type Headers = Record<string, string>
1111

12-
export const getAuthHeaders = (options: SeamHttpOptions): Headers => {
12+
export const getAuthHeaders = (options: Options): Headers => {
13+
if ('publishableKey' in options) {
14+
return getAuthHeadersForPublishableKey(options.publishableKey)
15+
}
16+
1317
if (isSeamHttpOptionsWithApiKey(options)) {
1418
return getAuthHeadersForApiKey(options)
1519
}
@@ -19,7 +23,7 @@ export const getAuthHeaders = (options: SeamHttpOptions): Headers => {
1923
}
2024

2125
throw new SeamHttpInvalidOptionsError(
22-
'Must specify an apiKey or clientSessionToken',
26+
'Must specify an apiKey, clientSessionToken, or publishableKey',
2327
)
2428
}
2529

@@ -42,6 +46,12 @@ const getAuthHeadersForApiKey = ({
4246
)
4347
}
4448

49+
if (isPublishableKey(apiKey)) {
50+
throw new SeamHttpInvalidTokenError(
51+
'A Publishable Key cannot be used as an apiKey',
52+
)
53+
}
54+
4555
if (!isSeamToken(apiKey)) {
4656
throw new SeamHttpInvalidTokenError(
4757
`Unknown or invalid apiKey format, expected token to start with ${tokenPrefix}`,
@@ -68,6 +78,12 @@ const getAuthHeadersForClientSessionToken = ({
6878
)
6979
}
7080

81+
if (isPublishableKey(clientSessionToken)) {
82+
throw new SeamHttpInvalidTokenError(
83+
'A Publishable Key cannot be used as a clientSessionToken',
84+
)
85+
}
86+
7187
if (!isClientSessionToken(clientSessionToken)) {
7288
throw new SeamHttpInvalidTokenError(
7389
`Unknown or invalid clientSessionToken format, expected token to start with ${clientSessionTokenPrefix}`,
@@ -80,6 +96,36 @@ const getAuthHeadersForClientSessionToken = ({
8096
}
8197
}
8298

99+
const getAuthHeadersForPublishableKey = (publishableKey: string): Headers => {
100+
if (isJwt(publishableKey)) {
101+
throw new SeamHttpInvalidTokenError(
102+
'A JWT cannot be used as a publishableKey',
103+
)
104+
}
105+
106+
if (isAccessToken(publishableKey)) {
107+
throw new SeamHttpInvalidTokenError(
108+
'An Access Token cannot be used as a publishableKey',
109+
)
110+
}
111+
112+
if (isClientSessionToken(publishableKey)) {
113+
throw new SeamHttpInvalidTokenError(
114+
'A Client Session Token Key cannot be used as a publishableKey',
115+
)
116+
}
117+
118+
if (!isPublishableKey(publishableKey)) {
119+
throw new SeamHttpInvalidTokenError(
120+
`Unknown or invalid publishableKey format, expected token to start with ${publishableKeyTokenPrefix}`,
121+
)
122+
}
123+
124+
return {
125+
'seam-publishable-key': publishableKey,
126+
}
127+
}
128+
83129
export class SeamHttpInvalidTokenError extends Error {
84130
constructor(message: string) {
85131
super(`SeamHttp received an invalid token: ${message}`)
@@ -88,10 +134,29 @@ export class SeamHttpInvalidTokenError extends Error {
88134
}
89135
}
90136

137+
export const warnOnInsecureuserIdentifierKey = (
138+
userIdentifierKey: string,
139+
): void => {
140+
if (isEmail(userIdentifierKey)) {
141+
// eslint-disable-next-line no-console
142+
console.warn(
143+
...[
144+
'Using an email for the userIdentifierKey is insecure and may return an error in the future!',
145+
'This is insecure because an email is common knowledge or easily guessed.',
146+
'Use something with sufficient entropy known only to the owner of the client session.',
147+
'For help choosing a user identifier key see',
148+
'https://docs.seam.co/latest/seam-components/overview/get-started-with-client-side-components#3-select-a-user-identifier-key',
149+
],
150+
)
151+
}
152+
}
153+
91154
const tokenPrefix = 'seam_'
92155

93156
const clientSessionTokenPrefix = 'seam_cst'
94157

158+
const publishableKeyTokenPrefix = 'seam_pk'
159+
95160
const isClientSessionToken = (token: string): boolean =>
96161
token.startsWith(clientSessionTokenPrefix)
97162

@@ -100,3 +165,10 @@ const isAccessToken = (token: string): boolean => token.startsWith('seam_at')
100165
const isJwt = (token: string): boolean => token.startsWith('ey')
101166

102167
const isSeamToken = (token: string): boolean => token.startsWith(tokenPrefix)
168+
169+
const isPublishableKey = (token: string): boolean =>
170+
token.startsWith(publishableKeyTokenPrefix)
171+
172+
// SOURCE: https://stackoverflow.com/a/46181
173+
const isEmail = (value: string): boolean =>
174+
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)

src/lib/seam/connect/client.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
1-
import axios, { type Axios } from 'axios'
2-
import axiosRetry, { exponentialDelay } from 'axios-retry'
1+
import axios, { type Axios, type AxiosRequestConfig } from 'axios'
2+
import axiosRetry, { type AxiosRetry, exponentialDelay } from 'axios-retry'
33

44
import { paramsSerializer } from 'lib/params-serializer.js'
55

6-
import { getAuthHeaders } from './auth.js'
7-
import {
8-
isSeamHttpOptionsWithClient,
9-
isSeamHttpOptionsWithClientSessionToken,
10-
type SeamHttpOptions,
11-
} from './options.js'
6+
export type Client = Axios
127

13-
export const createClient = (options: Required<SeamHttpOptions>): Axios => {
14-
if (isSeamHttpOptionsWithClient(options)) return options.client
8+
export interface ClientOptions {
9+
axiosOptions?: AxiosRequestConfig
10+
axiosRetryOptions?: AxiosRetryConfig
11+
client?: Client
12+
}
13+
14+
type AxiosRetryConfig = Parameters<AxiosRetry>[1]
15+
16+
export const createClient = (options: ClientOptions): Axios => {
17+
if (options.client != null) return options.client
1518

1619
const client = axios.create({
17-
baseURL: options.endpoint,
18-
withCredentials: isSeamHttpOptionsWithClientSessionToken(options),
1920
paramsSerializer,
2021
...options.axiosOptions,
21-
headers: {
22-
...getAuthHeaders(options),
23-
...options.axiosOptions.headers,
24-
},
2522
})
2623

2724
axiosRetry(client, {

src/lib/seam/connect/options.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
1-
import type { Axios, AxiosRequestConfig } from 'axios'
2-
import type { AxiosRetry } from 'axios-retry'
1+
import type { Client, ClientOptions } from './client.js'
32

43
export type SeamHttpOptions =
54
| SeamHttpOptionsFromEnv
65
| SeamHttpOptionsWithClient
76
| SeamHttpOptionsWithApiKey
87
| SeamHttpOptionsWithClientSessionToken
98

10-
interface SeamHttpCommonOptions {
9+
interface SeamHttpCommonOptions extends ClientOptions {
1110
endpoint?: string
12-
axiosOptions?: AxiosRequestConfig
13-
axiosRetryOptions?: AxiosRetryConfig
14-
enableLegacyMethodBehaivor?: boolean
1511
}
1612

17-
type AxiosRetryConfig = Parameters<AxiosRetry>[1]
13+
export type SeamHttpFromPublishableKeyOptions = SeamHttpCommonOptions
1814

1915
export type SeamHttpOptionsFromEnv = SeamHttpCommonOptions
2016

21-
export interface SeamHttpOptionsWithClient
22-
extends Pick<SeamHttpCommonOptions, 'enableLegacyMethodBehaivor'> {
23-
client: Axios
17+
export interface SeamHttpOptionsWithClient {
18+
client: Client
2419
}
2520

2621
export const isSeamHttpOptionsWithClient = (
@@ -29,12 +24,10 @@ export const isSeamHttpOptionsWithClient = (
2924
if (!('client' in options)) return false
3025
if (options.client == null) return false
3126

32-
const keys = Object.keys(options).filter(
33-
(k) => !['client', 'enableLegacyMethodBehaivor'].includes(k),
34-
)
27+
const keys = Object.keys(options).filter((k) => k !== 'client')
3528
if (keys.length > 0) {
3629
throw new SeamHttpInvalidOptionsError(
37-
`The client option cannot be used with any other option except enableLegacyMethodBehaivor, but received: ${keys.join(
30+
`The client option cannot be used with any other option, but received: ${keys.join(
3831
', ',
3932
)}`,
4033
)
@@ -55,7 +48,7 @@ export const isSeamHttpOptionsWithApiKey = (
5548

5649
if ('clientSessionToken' in options && options.clientSessionToken != null) {
5750
throw new SeamHttpInvalidOptionsError(
58-
'The clientSessionToken option cannot be used with the apiKey option.',
51+
'The clientSessionToken option cannot be used with the apiKey option',
5952
)
6053
}
6154

@@ -75,7 +68,7 @@ export const isSeamHttpOptionsWithClientSessionToken = (
7568

7669
if ('apiKey' in options && options.apiKey != null) {
7770
throw new SeamHttpInvalidOptionsError(
78-
'The clientSessionToken option cannot be used with the apiKey option.',
71+
'The clientSessionToken option cannot be used with the apiKey option',
7972
)
8073
}
8174

0 commit comments

Comments
 (0)