-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(inception): initial working version
- Loading branch information
0 parents
commit e51aaf9
Showing
13 changed files
with
6,737 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
name: Node.js Package | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
|
||
- uses: actions/setup-node@v3 | ||
with: | ||
node-version: 20 | ||
cache: yarn | ||
- run: yarn install --immutable | ||
- run: yarn build | ||
- run: yarn lint | ||
- run: yarn test | ||
|
||
|
||
publish-npm: | ||
needs: build | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v1 | ||
|
||
- uses: actions/setup-node@v3 | ||
with: | ||
node-version: 20 | ||
cache: yarn | ||
- run: yarn install --immutable | ||
- run: yarn build | ||
|
||
- name: Release | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
NODE_AUTH_TOKEN: ${{ secrets.SESAMECARE_OSS_NPM_TOKEN }} | ||
run: | | ||
yarn dlx semantic-release |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
name: Node CI | ||
|
||
on: [push] | ||
|
||
jobs: | ||
build: | ||
|
||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Use Node.js 18 | ||
uses: actions/setup-node@v3 | ||
with: | ||
node-version: 20 | ||
cache: yarn | ||
- name: npm install, lint, build, and test | ||
run: | | ||
yarn install --immutable | ||
yarn lint | ||
yarn build | ||
yarn test | ||
env: | ||
CI: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# Custom | ||
src/generated | ||
.transpiled | ||
.nyc_output | ||
.eslintcache | ||
|
||
# TypeScript incremental compilation cache | ||
*.tsbuildinfo | ||
|
||
# Stock | ||
*.seed | ||
*.log | ||
*.csv | ||
*.dat | ||
*.out | ||
*.pid | ||
*.gz | ||
*.orig | ||
|
||
work | ||
/build | ||
pids | ||
logs | ||
results | ||
coverage | ||
lib-cov | ||
html-report | ||
xunit.xml | ||
node_modules | ||
npm-debug.log | ||
|
||
.project | ||
.idea | ||
.settings | ||
.iml | ||
*.sublime-workspace | ||
*.sublime-project | ||
|
||
.DS_Store* | ||
ehthumbs.db | ||
Icon? | ||
Thumbs.db | ||
.AppleDouble | ||
.LSOverride | ||
.Spotlight-V100 | ||
.Trashes | ||
|
||
.yarn/* | ||
!.yarn/patches | ||
!.yarn/plugins | ||
!.yarn/releases | ||
!.yarn/sdks | ||
!.yarn/versions | ||
|
||
.node_repl_history | ||
|
||
# TypeScript incremental compilation cache | ||
*.tsbuildinfo | ||
# Added by coconfig | ||
.eslintignore | ||
.npmignore | ||
tsconfig.json | ||
tsconfig.build.json | ||
.prettierrc.js | ||
.eslintrc.js | ||
.commitlintrc.json | ||
vitest.config.ts |
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
nodeLinker: node-modules | ||
|
||
plugins: | ||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs | ||
spec: "@yarnpkg/plugin-interactive-tools" | ||
|
||
yarnPath: .yarn/releases/yarn-3.6.4.cjs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# fetch-with-token | ||
|
||
A simple wrapper around fetch that manages getting a token (typically oauth2 token or other refresh-able token) | ||
and updating it atomically when it expires or stops working. | ||
|
||
You just need to write the code to get a new token and the code to analyze whether a retry is required. | ||
|
||
```typescript | ||
import { createFetchFunction } from '@sesamecare-oss/fetch-with-token'; | ||
|
||
const partnerFetch = createFetchFunction({ | ||
async getToken() { | ||
const body = await fetch('https://auth.partner.com?clientId=foo&secret=bar').then((r) => r.json()); | ||
return { | ||
value: body.access_token, | ||
expiration: new Date(Date.now() + body.expires_in), | ||
}; | ||
}, | ||
}); | ||
|
||
// This call will go out with an authorization header | ||
const response = await partnerFetch('https://partner.com'); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
{ | ||
"name": "@sesamecare-oss/fetch-with-token", | ||
"version": "1.0.0", | ||
"description": "A simple wrapper around fetch that manages a token such as an oauth2 token and atomic refresh of expired tokens.", | ||
"main": "build/index.js", | ||
"types": "build/index.d.ts", | ||
"author": "Developers <developers@sesamecare.com>", | ||
"license": "UNLICENSED", | ||
"packageManager": "yarn@3.6.4", | ||
"scripts": { | ||
"build": "tsc -p tsconfig.build.json", | ||
"clean": "yarn dlx rimraf ./dist", | ||
"lint": "eslint .", | ||
"postinstall": "coconfig", | ||
"test": "vitest" | ||
}, | ||
"keywords": [ | ||
"typescript", | ||
"sesame", | ||
"oauth", | ||
"fetch" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/sesamecare/tokenized-fetch.git" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"release": { | ||
"branches": [ | ||
"main" | ||
], | ||
"plugins": [ | ||
"@semantic-release/commit-analyzer", | ||
"@semantic-release/release-notes-generator", | ||
"@semantic-release/changelog", | ||
[ | ||
"@semantic-release/exec", | ||
{ | ||
"publishCmd": "yarn dlx pinst --disable" | ||
} | ||
], | ||
"@semantic-release/npm", | ||
"@semantic-release/git" | ||
] | ||
}, | ||
"config": { | ||
"coconfig": "@openapi-typescript-infra/coconfig" | ||
}, | ||
"devDependencies": { | ||
"@openapi-typescript-infra/coconfig": "^4.2.1", | ||
"@semantic-release/changelog": "^6.0.3", | ||
"@semantic-release/exec": "^6.0.3", | ||
"@semantic-release/git": "^10.0.1", | ||
"@types/node": "^20.8.4", | ||
"@typescript-eslint/eslint-plugin": "^6.7.5", | ||
"@typescript-eslint/parser": "^6.7.5", | ||
"coconfig": "^0.13.3", | ||
"eslint": "^8.51.0", | ||
"eslint-config-prettier": "^9.0.0", | ||
"eslint-import-resolver-typescript": "^3.6.1", | ||
"eslint-plugin-import": "^2.28.1", | ||
"oauth2-mock-server": "^7.0.0", | ||
"typescript": "^5.2.2", | ||
"vitest": "^0.34.6" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { AbstractToken, FetchArugments, FetchWithTokenOptions } from './types'; | ||
|
||
function defaultOnFetch(token: AbstractToken, args: FetchArugments): FetchArugments { | ||
return { | ||
request: args.request, | ||
init: { | ||
...args.init, | ||
headers: { | ||
...args.init?.headers, | ||
Authorization: `Bearer ${token.value}`, | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
function defaultShouldReauthenticate(response: Response, retryCount: number): boolean { | ||
return response.status === 401 && retryCount < 2; | ||
} | ||
|
||
export function createFetchFunction<TokenType extends AbstractToken = AbstractToken>({ | ||
getToken, | ||
onFetch = defaultOnFetch, | ||
shouldReauthenticate = defaultShouldReauthenticate, | ||
}: FetchWithTokenOptions<TokenType>) { | ||
let tokenResolver: Promise<TokenType> | undefined; | ||
let currentToken: TokenType | undefined; | ||
|
||
async function resolveToken(force: boolean) { | ||
if (!force && currentToken && currentToken.expiration > new Date()) { | ||
return currentToken; | ||
} | ||
if (!tokenResolver) { | ||
const lastToken = currentToken; | ||
currentToken = undefined; | ||
tokenResolver = getToken(lastToken).then((token) => { | ||
currentToken = token; | ||
tokenResolver = undefined; | ||
return currentToken; | ||
}); | ||
} | ||
return tokenResolver; | ||
} | ||
|
||
async function fetchWithToken( | ||
request: FetchArugments['request'], | ||
init: FetchArugments['init'], | ||
retryCount: number = 0, | ||
): Promise<Response> { | ||
const token = await resolveToken(false); | ||
const modifiedFetchArguments = onFetch(token, { request, init }); | ||
const response = await fetch(modifiedFetchArguments.request, modifiedFetchArguments.init); | ||
if (shouldReauthenticate?.(response, retryCount)) { | ||
await resolveToken(true); | ||
return fetchWithToken(request, init, retryCount + 1); | ||
} | ||
return response; | ||
} | ||
|
||
return fetchWithToken as typeof fetch; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'; | ||
import { OAuth2Server } from 'oauth2-mock-server'; | ||
|
||
import { createFetchFunction } from './index'; | ||
|
||
describe('tokenized-fetch', () => { | ||
const server = new OAuth2Server(); | ||
|
||
beforeAll(async () => { | ||
await server.start(20230, 'localhost'); | ||
await server.issuer.keys.generate('RS256'); | ||
}); | ||
|
||
afterAll(async () => { | ||
await server.stop(); | ||
}); | ||
|
||
test('should manage simple oauth token refreshing', async () => { | ||
let getCount = 0; | ||
|
||
const fetcher = createFetchFunction({ | ||
async getToken() { | ||
getCount += 1; | ||
const response = await fetch('http://localhost:20230/token', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
}, | ||
body: new URLSearchParams({ | ||
grant_type: 'client_credentials', | ||
scope: 'urn:read', | ||
}).toString(), | ||
}); | ||
const token = await response.json(); | ||
return { | ||
value: token.access_token, | ||
expiration: new Date(Date.now() + token.expires_in), | ||
}; | ||
}, | ||
shouldReauthenticate(response, retryCount) { | ||
return response.status === 401 && retryCount < 2; | ||
}, | ||
}); | ||
|
||
async function introspect() { | ||
return fetcher('http://localhost:20230/introspect', { method: 'POST' }).then(async (response) => { | ||
return { | ||
status: response.status, | ||
json: await response.json(), | ||
}; | ||
}); | ||
} | ||
|
||
await expect(introspect()).resolves.toEqual({ status: 200, json: { active: true } }); | ||
expect(getCount, 'Should have called getToken').toBe(1); | ||
|
||
await expect(introspect()).resolves.toEqual({ status: 200, json: { active: true } }); | ||
expect(getCount, 'Should not have called getToken again').toBe(1); | ||
|
||
server.service.once('beforeIntrospect', (introspectResponse) => { | ||
introspectResponse.statusCode = 401; | ||
introspectResponse.body = { active: false }; | ||
}); | ||
|
||
const double = await Promise.all([ | ||
introspect(), | ||
introspect(), | ||
]); | ||
expect(getCount, 'should have refreshed the token once').toBe(2); | ||
expect(double).toMatchInlineSnapshot(` | ||
[ | ||
{ | ||
"json": { | ||
"active": true, | ||
}, | ||
"status": 200, | ||
}, | ||
{ | ||
"json": { | ||
"active": true, | ||
}, | ||
"status": 200, | ||
}, | ||
] | ||
`); | ||
|
||
server.service.on('beforeIntrospect', (introspectResponse) => { | ||
introspectResponse.statusCode = 401; | ||
introspectResponse.body = { active: false }; | ||
}); | ||
await expect(introspect(), 'should not retry infinitely').resolves.toEqual({ status: 401, json: { active: false } }); | ||
expect(getCount, 'should have attempted to refresh 2 more times').toBe(4); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './types'; | ||
export * from './fetch'; |
Oops, something went wrong.