Skip to content

Commit

Permalink
Merge branch 'main' into feat/fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
AmeanAsad committed Oct 12, 2023
2 parents a3dec56 + 21fab07 commit 747d780
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 17 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
branches:
- main
jobs:
cicd:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ This is the official JavaScript client for Filecoin Saturn. It is a work in prog
## Installation

```bash
npm install strn
npm install @filecoin-saturn/js-client
```

## Usage

```js
import Saturn from 'strn'
import Saturn from '@filecoin-saturn/js-client'

const client = new Saturn()

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@filecoin-saturn/js-client",
"version": "0.0.9",
"version": "0.1.0",
"description": "Filecoin Saturn Client",
"homepage": "https://github.com/filecoin-saturn/js-client",
"main": "src/index.js",
Expand Down
25 changes: 23 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { CID } from 'multiformats'
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 @@ -27,12 +30,17 @@ class Saturn {
downloadTimeout: 0
}, opts)

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

this.logs = []
this.storage = this.opts.storage
this.nodes = []
this.nodesListKey = 'saturn-nodes'
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 @@ -53,7 +61,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 @@ -66,6 +76,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 @@ -165,6 +182,10 @@ class Saturn {
url.searchParams.set('dag-scope', 'entity')
}

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

return url
}

Expand Down
16 changes: 16 additions & 0 deletions src/storage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @ts-check

import { indexedDbStorage } from './indexed-db-storage.js'
import { memoryStorage } from './memory-storage.js'

/**
* @typedef {object} Storage
* @property {function(string):Promise<any>} get - Retrieves the value associated with the key.
* @property {function(string,any):Promise<void>} set - Sets a new value for the key.
* @property {function(string):Promise<any>} delete - Deletes the value associated with the key.
*/

export {
indexedDbStorage,
memoryStorage
}
29 changes: 29 additions & 0 deletions src/storage/indexed-db-storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// @ts-check

import { openDB } from 'idb'

const DEFAULT_IDB_VERSION = 1
const DEFAULT_IDB_STORAGE_NAME = 'saturn-db'
const DEFAULT_SATURN_STORAGE_NAME = 'saturn-client'

/**
* @function indexedDbStorage
* @returns {import('./index.js').Storage}
*/
export function indexedDbStorage () {
const indexedDbExists = (typeof window !== 'undefined') && window?.indexedDB
let dbPromise
if (indexedDbExists) {
dbPromise = openDB(DEFAULT_IDB_STORAGE_NAME, DEFAULT_IDB_VERSION, {
upgrade (db) {
db.createObjectStore(DEFAULT_SATURN_STORAGE_NAME)
}
})
}

return {
get: async (key) => indexedDbExists && (await dbPromise).get(DEFAULT_SATURN_STORAGE_NAME, key),
set: async (key, value) => indexedDbExists && (await dbPromise).put(DEFAULT_SATURN_STORAGE_NAME, value, key),
delete: async (key) => indexedDbExists && (await dbPromise).delete(DEFAULT_SATURN_STORAGE_NAME, key)
}
}
15 changes: 15 additions & 0 deletions src/storage/memory-storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @ts-check

/**
* @function memoryStorage
* @returns {import('./index.js').Storage}
*/
export function memoryStorage () {
const storageObject = {}

return {
get: async (key) => storageObject[key],
set: async (key, value) => { storageObject[key] = value },
delete: async (key) => { delete storageObject[key] }
}
}
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
}
18 changes: 10 additions & 8 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() // eslint-disable-line
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 All @@ -76,7 +78,7 @@ describe('Saturn client', () => {
await assert.rejects(client.fetchContentBuffer(TEST_CID, { connectTimeout: 1 }))

const log = client.logs.pop()
assert.strictEqual(log.ifNetworkError, 'This operation was aborted')
assert.strictEqual(log.error, 'This operation was aborted')
})
})
})
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 747d780

Please sign in to comment.