Skip to content

Commit d6d175b

Browse files
razor-xseambot
andauthored
Add console session token and personal access token support (#19)
Co-authored-by: Seam Bot <[email protected]>
1 parent bae12d3 commit d6d175b

28 files changed

+1334
-5
lines changed

README.md

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,23 @@ JavaScript HTTP client for the Seam API written in TypeScript.
77

88
## Description
99

10-
TODO
10+
[Seam] makes it easy to integrate IoT devices with your applications.
11+
This is an official SDK for the Seam API.
12+
Please refer to the official [Seam Docs] to get started.
13+
14+
Parts of this SDK are generated from always up-to-date type information
15+
provided by [@seamapi/types].
16+
This ensures all API methods, request shapes, and response shapes are
17+
accurate and fully typed.
18+
19+
The SDK contains minimal dependencies, is fully tree-shakeable,
20+
and optimized for use in both client and server applications.
21+
The underlying HTTP client is [Axios].
22+
23+
[Seam]: https://www.seam.co/
24+
[Seam Docs]: https://docs.seam.co/latest/
25+
[@seamapi/types]: https://github.com/seamapi/types/
26+
[Axios]: https://axios-http.com/
1127

1228
## Installation
1329

@@ -19,6 +35,137 @@ $ npm install @seamapi/http
1935

2036
[npm]: https://www.npmjs.com/
2137

38+
## Usage
39+
40+
```ts
41+
import { SeamHttp } from '@seamapi/http'
42+
43+
const seam = new SeamHttp('your-api-key')
44+
const devices = await seam.devices.list()
45+
```
46+
47+
### Authentication Methods
48+
49+
The SDK supports several authentication mechanisms.
50+
Authentication may be configured by passing the corresponding
51+
options directly to the `SeamHttp` constructor,
52+
or with the more ergonomic static factory methods.
53+
54+
> Publishable Key authentication is not supported by the constructor
55+
> and must be configured using `SeamHttp.fromPublishableKey`.
56+
57+
#### API Key
58+
59+
An API key is scoped to a single workspace and should only be used on the server.
60+
Obtain one from the Seam Console.
61+
62+
##### Set the `SEAM_API_KEY` environment variable
63+
64+
```ts
65+
const seam = new SeamHttp()
66+
```
67+
68+
##### Pass as the first argument to the constructor
69+
70+
```ts
71+
const seam = new SeamHttp('your-api-key')
72+
```
73+
74+
##### Pass as an option the constructor
75+
76+
```ts
77+
const seam = new SeamHttp({ apiKey: 'your-api-key' })
78+
```
79+
80+
##### Use the factory method
81+
82+
```ts
83+
const seam = SeamHttp.fromApiKey('your-api-key')
84+
```
85+
86+
#### Client Session Token
87+
88+
A Client Session Token is scoped to a client session and should only be used on the client.
89+
90+
##### Pass as an option the constructor
91+
92+
```ts
93+
const seam = new SeamHttp({ clientSessionToken: 'some-client-session-token' })
94+
```
95+
96+
##### Use the factory method
97+
98+
```ts
99+
const seam = SeamHttp.fromClientSessionToken('some-client-session-token')
100+
```
101+
102+
#### Publishable Key
103+
104+
A Publishable Key is used by the client to acquire Client Session Token for a workspace.
105+
Obtain one from the Seam Console.
106+
107+
Use the async factory method to return a client authenticated with a client session token:
108+
109+
```ts
110+
const seam = await SeamHttp.fromPublishableKey(
111+
'your-publishable-key',
112+
'some-user-identifier-key',
113+
)
114+
```
115+
116+
This will get an existing client session matching the user identifier key,
117+
or create a new empty client session.
118+
119+
#### Personal Access Token
120+
121+
A Personal Access Token is scoped to a Seam Console user.
122+
Obtain one from the Seam Console.
123+
A workspace id must be provided when using this method
124+
and all requests will be scoped to that workspace.
125+
126+
##### Pass as an option the constructor
127+
128+
```ts
129+
const seam = new SeamHttp({
130+
personalAccessToken: 'your-personal-access-token',
131+
workspaceId: 'your-workspace-id',
132+
})
133+
```
134+
135+
##### Use the factory method
136+
137+
```ts
138+
const seam = SeamHttp.fromPersonalAccessToken(
139+
'some-console-session-token',
140+
'your-workspace-id',
141+
)
142+
```
143+
144+
#### Console Session Token
145+
146+
A Console Session Token is used by the Seam Console.
147+
This authentication method is only used by internal Seam applications.
148+
A workspace id must be provided when using this method
149+
and all requests will be scoped to that workspace.
150+
151+
##### Pass as an option the constructor
152+
153+
```ts
154+
const seam = new SeamHttp({
155+
consoleSessionToken: 'some-console-session-token',
156+
workspaceId: 'your-workspace-id',
157+
})
158+
```
159+
160+
##### Use the factory method
161+
162+
```ts
163+
const seam = SeamHttp.fromConsoleSessionToken(
164+
'some-console-session-token',
165+
'your-workspace-id',
166+
)
167+
```
168+
22169
## Development and Testing
23170

24171
### Quickstart

generate-routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,12 +243,16 @@ import {
243243
isSeamHttpOptionsWithApiKey,
244244
isSeamHttpOptionsWithClient,
245245
isSeamHttpOptionsWithClientSessionToken,
246+
isSeamHttpOptionsWithConsoleSessionToken,
247+
isSeamHttpOptionsWithPersonalAccessToken,
246248
type SeamHttpFromPublishableKeyOptions,
247249
SeamHttpInvalidOptionsError,
248250
type SeamHttpOptions,
249251
type SeamHttpOptionsWithApiKey,
250252
type SeamHttpOptionsWithClient,
251253
type SeamHttpOptionsWithClientSessionToken,
254+
type SeamHttpOptionsWithConsoleSessionToken,
255+
type SeamHttpOptionsWithPersonalAccessToken,
252256
} from 'lib/seam/connect/options.js'
253257
import { parseOptions } from 'lib/seam/connect/parse-options.js'
254258

src/lib/seam/connect/auth.ts

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import {
22
isSeamHttpOptionsWithApiKey,
33
isSeamHttpOptionsWithClientSessionToken,
4+
isSeamHttpOptionsWithConsoleSessionToken,
5+
isSeamHttpOptionsWithPersonalAccessToken,
46
SeamHttpInvalidOptionsError,
57
type SeamHttpOptionsWithApiKey,
68
type SeamHttpOptionsWithClientSessionToken,
9+
type SeamHttpOptionsWithConsoleSessionToken,
10+
type SeamHttpOptionsWithPersonalAccessToken,
711
} from './options.js'
812
import type { Options } from './parse-options.js'
913

@@ -22,8 +26,23 @@ export const getAuthHeaders = (options: Options): Headers => {
2226
return getAuthHeadersForClientSessionToken(options)
2327
}
2428

29+
if (isSeamHttpOptionsWithConsoleSessionToken(options)) {
30+
return getAuthHeadersForConsoleSessionToken(options)
31+
}
32+
33+
if (isSeamHttpOptionsWithPersonalAccessToken(options)) {
34+
return getAuthHeadersForPersonalAccessToken(options)
35+
}
36+
2537
throw new SeamHttpInvalidOptionsError(
26-
'Must specify an apiKey, clientSessionToken, or publishableKey',
38+
[
39+
'Must specify',
40+
'an apiKey,',
41+
'clientSessionToken,',
42+
'publishableKey,',
43+
'consoleSessionToken with a workspaceId',
44+
'or personalAccessToken with a workspaceId',
45+
].join(' '),
2746
)
2847
}
2948

@@ -96,6 +115,74 @@ const getAuthHeadersForClientSessionToken = ({
96115
}
97116
}
98117

118+
const getAuthHeadersForConsoleSessionToken = ({
119+
consoleSessionToken,
120+
workspaceId,
121+
}: SeamHttpOptionsWithConsoleSessionToken): Headers => {
122+
if (isAccessToken(consoleSessionToken)) {
123+
throw new SeamHttpInvalidTokenError(
124+
'An Access Token cannot be used as a consoleSessionToken',
125+
)
126+
}
127+
128+
if (isClientSessionToken(consoleSessionToken)) {
129+
throw new SeamHttpInvalidTokenError(
130+
'A Client Session Token cannot be used as a consoleSessionToken',
131+
)
132+
}
133+
134+
if (isPublishableKey(consoleSessionToken)) {
135+
throw new SeamHttpInvalidTokenError(
136+
'A Publishable Key cannot be used as a consoleSessionToken',
137+
)
138+
}
139+
140+
if (!isJwt(consoleSessionToken)) {
141+
throw new SeamHttpInvalidTokenError(
142+
`Unknown or invalid consoleSessionToken format, expected a JWT which starts with ${jwtPrefix}`,
143+
)
144+
}
145+
146+
return {
147+
authorization: `Bearer ${consoleSessionToken}`,
148+
'seam-workspace-id': workspaceId,
149+
}
150+
}
151+
152+
const getAuthHeadersForPersonalAccessToken = ({
153+
personalAccessToken,
154+
workspaceId,
155+
}: SeamHttpOptionsWithPersonalAccessToken): Headers => {
156+
if (isJwt(personalAccessToken)) {
157+
throw new SeamHttpInvalidTokenError(
158+
'A JWT cannot be used as a personalAccessToken',
159+
)
160+
}
161+
162+
if (isClientSessionToken(personalAccessToken)) {
163+
throw new SeamHttpInvalidTokenError(
164+
'A Client Session Token cannot be used as a personalAccessToken',
165+
)
166+
}
167+
168+
if (isPublishableKey(personalAccessToken)) {
169+
throw new SeamHttpInvalidTokenError(
170+
'A Publishable Key cannot be used as a personalAccessToken',
171+
)
172+
}
173+
174+
if (!isAccessToken(personalAccessToken)) {
175+
throw new SeamHttpInvalidTokenError(
176+
`Unknown or invalid personalAccessToken format, expected token to start with ${accessTokenPrefix}`,
177+
)
178+
}
179+
180+
return {
181+
authorization: `Bearer ${personalAccessToken}`,
182+
'seam-workspace-id': workspaceId,
183+
}
184+
}
185+
99186
const getAuthHeadersForPublishableKey = (publishableKey: string): Headers => {
100187
if (isJwt(publishableKey)) {
101188
throw new SeamHttpInvalidTokenError(
@@ -153,16 +240,21 @@ export const warnOnInsecureuserIdentifierKey = (
153240

154241
const tokenPrefix = 'seam_'
155242

243+
const accessTokenPrefix = 'seam_at'
244+
245+
const jwtPrefix = 'ey'
246+
156247
const clientSessionTokenPrefix = 'seam_cst'
157248

158249
const publishableKeyTokenPrefix = 'seam_pk'
159250

160251
const isClientSessionToken = (token: string): boolean =>
161252
token.startsWith(clientSessionTokenPrefix)
162253

163-
const isAccessToken = (token: string): boolean => token.startsWith('seam_at')
254+
const isAccessToken = (token: string): boolean =>
255+
token.startsWith(accessTokenPrefix)
164256

165-
const isJwt = (token: string): boolean => token.startsWith('ey')
257+
const isJwt = (token: string): boolean => token.startsWith(jwtPrefix)
166258

167259
const isSeamToken = (token: string): boolean => token.startsWith(tokenPrefix)
168260

0 commit comments

Comments
 (0)