Skip to content

Commit 4bc09dd

Browse files
authored
feat: basic jwt support (#17)
1 parent b76ea52 commit 4bc09dd

File tree

4 files changed

+84
-9
lines changed

4 files changed

+84
-9
lines changed

src/index.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { extractVerifiedContent } from './utils/car.js'
44
import { asAsyncIterable, asyncIteratorToBuffer } from './utils/itr.js'
55
import { randomUUID } from './utils/uuid.js'
66
import { memoryStorage } from './storage/index.js'
7+
import { getJWT } from './utils/jwt.js'
78

89
class Saturn {
910
/**
1011
*
1112
* @param {object} [opts={}]
13+
* @param {string} [opts.clientKey]
1214
* @param {string} [opts.clientId=randomUUID()]
1315
* @param {string} [opts.cdnURL=saturn.ms]
1416
* @param {number} [opts.connectTimeout=5000]
@@ -20,15 +22,20 @@ class Saturn {
2022
clientId: randomUUID(),
2123
cdnURL: 'saturn.ms',
2224
logURL: 'https://twb3qukm2i654i3tnvx36char40aymqq.lambda-url.us-west-2.on.aws/',
25+
authURL: 'https://fz3dyeyxmebszwhuiky7vggmsu0rlkoy.lambda-url.us-west-2.on.aws/',
2326
connectTimeout: 5_000,
2427
downloadTimeout: 0
25-
2628
}, opts)
2729

30+
if (!this.opts.clientKey) {
31+
throw new Error('clientKey is required')
32+
}
33+
2834
this.logs = []
2935
this.storage = this.opts.storage || memoryStorage()
3036
this.reportingLogs = process?.env?.NODE_ENV !== 'development'
3137
this.hasPerformanceAPI = typeof window !== 'undefined' && window?.performance
38+
this.isBrowser = typeof window !== 'undefined'
3239
if (this.reportingLogs && this.hasPerformanceAPI) {
3340
this._monitorPerformanceBuffer()
3441
}
@@ -47,7 +54,9 @@ class Saturn {
4754
const [cid] = (cidPath ?? '').split('/')
4855
CID.parse(cid)
4956

50-
const options = Object.assign({}, this.opts, { format: 'car' }, opts)
57+
const jwt = await getJWT(this.opts, this.storage)
58+
59+
const options = Object.assign({}, this.opts, { format: 'car', jwt }, opts)
5160
const url = this.createRequestURL(cidPath, options)
5261

5362
const log = {
@@ -60,6 +69,13 @@ class Saturn {
6069
controller.abort()
6170
}, options.connectTimeout)
6271

72+
if (!this.isBrowser) {
73+
options.headers = {
74+
...(options.headers || {}),
75+
Authorization: 'Bearer ' + options.jwt
76+
}
77+
}
78+
6379
let res
6480
try {
6581
res = await fetch(url, { signal: controller.signal, ...options })
@@ -159,6 +175,10 @@ class Saturn {
159175
url.searchParams.set('dag-scope', 'entity')
160176
}
161177

178+
if (this.isBrowser) {
179+
url.searchParams.set('jwt', opts.jwt)
180+
}
181+
162182
return url
163183
}
164184

src/utils/jwt.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { base64 } from 'multiformats/bases/base64'
2+
import { bytes } from 'multiformats'
3+
4+
const JWT_KEY = 'strn/jwt'
5+
6+
/**
7+
* @param {string} jwt
8+
*/
9+
export function isJwtValid (jwt) {
10+
if (!jwt) return false
11+
const { exp } = JSON.parse(bytes.toString(base64.decode('m' + jwt.split('.')[1])))
12+
return Date.now() < exp * 1000
13+
}
14+
15+
/**
16+
* @param {object} opts
17+
* @param {string} opts.clientKey
18+
* @param {string} opts.authURL
19+
* @param {import('./utils/storage.js').Storage} storage
20+
* @returns {Promise<string>}
21+
*/
22+
export async function getJWT (opts, storage) {
23+
try {
24+
const jwt = await storage.get(JWT_KEY)
25+
if (isJwtValid(jwt)) return jwt
26+
} catch (e) {
27+
}
28+
29+
const { clientKey, authURL } = opts
30+
const url = `${authURL}?clientKey=${clientKey}`
31+
32+
const result = await fetch(url)
33+
const { token, message } = await result.json()
34+
35+
if (!token) throw new Error(message || 'Failed to refresh jwt')
36+
37+
try {
38+
await storage.set(JWT_KEY, token)
39+
} catch (e) {
40+
}
41+
42+
return token
43+
}

test/index.spec.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,39 @@ import Saturn from '#src/index.js'
66

77
const TEST_CID = 'QmXjYBY478Cno4jzdCcPy4NcJYFrwHZ51xaCP8vUwN9MGm'
88

9+
const clientKey = 'abc123'
10+
911
describe('Saturn client', () => {
1012
describe('constructor', () => {
1113
it('should work w/o custom client ID', () => {
12-
new Saturn()
14+
new Saturn({ clientKey })
1315
})
1416

1517
it('should work with custom client ID', () => {
1618
const clientId = randomUUID()
17-
const saturn = new Saturn({ clientId })
19+
const saturn = new Saturn({ clientId, clientKey })
1820
assert.strictEqual(saturn.opts.clientId, clientId)
1921
})
2022

2123
it('should work with custom CDN URL', () => {
2224
const cdnURL = 'custom.com'
23-
const saturn = new Saturn({ cdnURL })
25+
const saturn = new Saturn({ cdnURL, clientKey })
2426
assert.strictEqual(saturn.opts.cdnURL, cdnURL)
2527
})
2628

2729
it('should work with custom connect timeout', () => {
28-
const saturn = new Saturn({ connectTimeout: 1234 })
30+
const saturn = new Saturn({ connectTimeout: 1234, clientKey })
2931
assert.strictEqual(saturn.opts.connectTimeout, 1234)
3032
})
3133

3234
it('should work with custom download timeout', () => {
33-
const saturn = new Saturn({ downloadTimeout: 3456 })
35+
const saturn = new Saturn({ downloadTimeout: 3456, clientKey })
3436
assert.strictEqual(saturn.opts.downloadTimeout, 3456)
3537
})
3638
})
3739

3840
describe('Fetch a CID', () => {
39-
const client = new Saturn()
41+
const client = new Saturn({ clientKey })
4042

4143
it('should fetch test CID', async () => {
4244
const { res } = await client.fetchCID(TEST_CID)
@@ -57,7 +59,7 @@ describe('Saturn client', () => {
5759
})
5860

5961
describe('Logging', () => {
60-
const client = new Saturn()
62+
const client = new Saturn({ clientKey })
6163
client.reportingLogs = true
6264

6365
it('should create a log on fetch success', async () => {

test/jwt.spec.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { isJwtValid } from '#src/utils/jwt.js'
2+
import { describe, it } from 'node:test'
3+
import assert from 'node:assert/strict'
4+
5+
describe('JWT tests', () => {
6+
it('should validate a jwt', () => {
7+
const fixture = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2NGQ1ZGI0ZC1jYmQ3LTRkYWMtOWY4Zi01NGQyMjk0OGE3Y2UiLCJzdWIiOiJhYmMxMjMiLCJzdWJUeXBlIjoiY2xpZW50S2V5IiwiYWxsb3dfbGlzdCI6WyIqIl0sImlhdCI6MTY5NjQ3MTQ5MSwiZXhwIjoxNjk2NDc1MDkxfQ.ZJeuzb6JucwUarI7_MlomTjow4Lc4RHZsPhqDepT1q6Pxs5KNVeOQwdZeCDqFSa8QQTiK-VHoKtDH7x349F5QA'
8+
assert.equal(isJwtValid(fixture), false)
9+
})
10+
})

0 commit comments

Comments
 (0)