Skip to content

Commit

Permalink
feat: basic jwt support (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
tchardin authored Oct 11, 2023
1 parent b76ea52 commit 4bc09dd
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 9 deletions.
24 changes: 22 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { extractVerifiedContent } from './utils/car.js'
import { asAsyncIterable, asyncIteratorToBuffer } from './utils/itr.js'
import { randomUUID } from './utils/uuid.js'
import { memoryStorage } from './storage/index.js'
import { getJWT } from './utils/jwt.js'

class Saturn {
/**
*
* @param {object} [opts={}]
* @param {string} [opts.clientKey]
* @param {string} [opts.clientId=randomUUID()]
* @param {string} [opts.cdnURL=saturn.ms]
* @param {number} [opts.connectTimeout=5000]
Expand All @@ -20,15 +22,20 @@ class Saturn {
clientId: randomUUID(),
cdnURL: 'saturn.ms',
logURL: 'https://twb3qukm2i654i3tnvx36char40aymqq.lambda-url.us-west-2.on.aws/',
authURL: 'https://fz3dyeyxmebszwhuiky7vggmsu0rlkoy.lambda-url.us-west-2.on.aws/',
connectTimeout: 5_000,
downloadTimeout: 0

}, opts)

if (!this.opts.clientKey) {
throw new Error('clientKey is required')
}

this.logs = []
this.storage = this.opts.storage || memoryStorage()
this.reportingLogs = process?.env?.NODE_ENV !== 'development'
this.hasPerformanceAPI = typeof window !== 'undefined' && window?.performance
this.isBrowser = typeof window !== 'undefined'
if (this.reportingLogs && this.hasPerformanceAPI) {
this._monitorPerformanceBuffer()
}
Expand All @@ -47,7 +54,9 @@ class Saturn {
const [cid] = (cidPath ?? '').split('/')
CID.parse(cid)

const options = Object.assign({}, this.opts, { format: 'car' }, opts)
const jwt = await getJWT(this.opts, this.storage)

const options = Object.assign({}, this.opts, { format: 'car', jwt }, opts)
const url = this.createRequestURL(cidPath, options)

const log = {
Expand All @@ -60,6 +69,13 @@ class Saturn {
controller.abort()
}, options.connectTimeout)

if (!this.isBrowser) {
options.headers = {
...(options.headers || {}),
Authorization: 'Bearer ' + options.jwt
}
}

let res
try {
res = await fetch(url, { signal: controller.signal, ...options })
Expand Down Expand Up @@ -159,6 +175,10 @@ class Saturn {
url.searchParams.set('dag-scope', 'entity')
}

if (this.isBrowser) {
url.searchParams.set('jwt', opts.jwt)
}

return url
}

Expand Down
43 changes: 43 additions & 0 deletions src/utils/jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { base64 } from 'multiformats/bases/base64'
import { bytes } from 'multiformats'

const JWT_KEY = 'strn/jwt'

/**
* @param {string} jwt
*/
export function isJwtValid (jwt) {
if (!jwt) return false
const { exp } = JSON.parse(bytes.toString(base64.decode('m' + jwt.split('.')[1])))
return Date.now() < exp * 1000
}

/**
* @param {object} opts
* @param {string} opts.clientKey
* @param {string} opts.authURL
* @param {import('./utils/storage.js').Storage} storage
* @returns {Promise<string>}
*/
export async function getJWT (opts, storage) {
try {
const jwt = await storage.get(JWT_KEY)
if (isJwtValid(jwt)) return jwt
} catch (e) {
}

const { clientKey, authURL } = opts
const url = `${authURL}?clientKey=${clientKey}`

const result = await fetch(url)
const { token, message } = await result.json()

if (!token) throw new Error(message || 'Failed to refresh jwt')

try {
await storage.set(JWT_KEY, token)
} catch (e) {
}

return token
}
16 changes: 9 additions & 7 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,39 @@ import Saturn from '#src/index.js'

const TEST_CID = 'QmXjYBY478Cno4jzdCcPy4NcJYFrwHZ51xaCP8vUwN9MGm'

const clientKey = 'abc123'

describe('Saturn client', () => {
describe('constructor', () => {
it('should work w/o custom client ID', () => {
new Saturn()
new Saturn({ clientKey })
})

it('should work with custom client ID', () => {
const clientId = randomUUID()
const saturn = new Saturn({ clientId })
const saturn = new Saturn({ clientId, clientKey })
assert.strictEqual(saturn.opts.clientId, clientId)
})

it('should work with custom CDN URL', () => {
const cdnURL = 'custom.com'
const saturn = new Saturn({ cdnURL })
const saturn = new Saturn({ cdnURL, clientKey })
assert.strictEqual(saturn.opts.cdnURL, cdnURL)
})

it('should work with custom connect timeout', () => {
const saturn = new Saturn({ connectTimeout: 1234 })
const saturn = new Saturn({ connectTimeout: 1234, clientKey })
assert.strictEqual(saturn.opts.connectTimeout, 1234)
})

it('should work with custom download timeout', () => {
const saturn = new Saturn({ downloadTimeout: 3456 })
const saturn = new Saturn({ downloadTimeout: 3456, clientKey })
assert.strictEqual(saturn.opts.downloadTimeout, 3456)
})
})

describe('Fetch a CID', () => {
const client = new Saturn()
const client = new Saturn({ clientKey })

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

describe('Logging', () => {
const client = new Saturn()
const client = new Saturn({ clientKey })
client.reportingLogs = true

it('should create a log on fetch success', async () => {
Expand Down
10 changes: 10 additions & 0 deletions test/jwt.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { isJwtValid } from '#src/utils/jwt.js'
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'

describe('JWT tests', () => {
it('should validate a jwt', () => {
const fixture = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2NGQ1ZGI0ZC1jYmQ3LTRkYWMtOWY4Zi01NGQyMjk0OGE3Y2UiLCJzdWIiOiJhYmMxMjMiLCJzdWJUeXBlIjoiY2xpZW50S2V5IiwiYWxsb3dfbGlzdCI6WyIqIl0sImlhdCI6MTY5NjQ3MTQ5MSwiZXhwIjoxNjk2NDc1MDkxfQ.ZJeuzb6JucwUarI7_MlomTjow4Lc4RHZsPhqDepT1q6Pxs5KNVeOQwdZeCDqFSa8QQTiK-VHoKtDH7x349F5QA'
assert.equal(isJwtValid(fixture), false)
})
})

0 comments on commit 4bc09dd

Please sign in to comment.