Skip to content

Commit

Permalink
feat(node): node sdk (#338)
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie committed Jul 14, 2022
1 parent 518fe79 commit 2cb03c1
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 7 deletions.
2 changes: 1 addition & 1 deletion packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { buildAccessTokenKey, getDiscoveryEndpoint } from './utils';
export type { IdTokenClaims, LogtoErrorCode } from '@logto/js';
export { LogtoError, OidcError, Prompt, LogtoRequestError } from '@logto/js';
export * from './errors';
export type { Storage, StorageKey } from './adapter';
export type { Storage, StorageKey, ClientAdapter } from './adapter';
export { createRequester } from './utils';

export type LogtoConfig = {
Expand Down
10 changes: 10 additions & 0 deletions packages/node/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
preset: 'ts-jest',
collectCoverageFrom: ['src/**/*.ts'],
coverageReporters: ['lcov', 'text-summary'],
setupFilesAfterEnv: ['jest-matcher-specific-error'],
};

export default config;
68 changes: 68 additions & 0 deletions packages/node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"name": "@logto/node",
"version": "1.0.0-alpha.2",
"source": "./src/index.ts",
"main": "./lib/index.js",
"exports": {
"require": "./lib/index.js",
"import": "./lib/module.js"
},
"module": "./lib/module.js",
"types": "./lib/index.d.ts",
"files": [
"lib"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/logto-io/js.git",
"directory": "packages/node"
},
"scripts": {
"dev:tsc": "tsc -p tsconfig.build.json -w --preserveWatchOutput",
"precommit": "lint-staged",
"check": "tsc --noEmit",
"build": "rm -rf lib/ && pnpm check && parcel build",
"lint": "eslint --ext .ts src",
"test": "jest",
"test:coverage": "jest --silent --coverage",
"prepack": "pnpm test"
},
"dependencies": {
"@logto/client": "^1.0.0-alpha.2",
"@silverhand/essentials": "^1.1.6",
"js-base64": "^3.7.2",
"node-fetch": "^2.6.7"
},
"devDependencies": {
"@jest/types": "^27.5.1",
"@parcel/core": "^2.6.2",
"@parcel/packager-ts": "^2.6.2",
"@parcel/transformer-typescript-types": "^2.6.2",
"@silverhand/eslint-config": "^0.14.0",
"@silverhand/ts-config": "^0.14.0",
"@types/jest": "^27.4.0",
"eslint": "^8.9.0",
"jest": "^27.5.1",
"jest-location-mock": "^1.0.9",
"jest-matcher-specific-error": "^1.0.0",
"lint-staged": "^13.0.0",
"parcel": "^2.6.2",
"prettier": "^2.3.2",
"ts-jest": "^27.0.4",
"typescript": "^4.5.5"
},
"eslintConfig": {
"extends": "@silverhand"
},
"prettier": "@silverhand/eslint-config/.prettierrc",
"publishConfig": {
"access": "public"
},
"targets": {
"main": {
"context": "node",
"includeNodeModules": false
}
}
}
4 changes: 4 additions & 0 deletions packages/node/src/include.d/node-fetch.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module 'node-fetch' {
const nodeFetch: typeof fetch;
export = nodeFetch;
}
19 changes: 19 additions & 0 deletions packages/node/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import LogtoClient from '.';

const appId = 'app_id_value';
const endpoint = 'https://logto.dev';

const navigate = jest.fn();
const storage = {
setItem: jest.fn(),
getItem: jest.fn(),
removeItem: jest.fn(),
};

describe('LogtoClient', () => {
describe('constructor', () => {
it('constructor should not throw', () => {
expect(() => new LogtoClient({ endpoint, appId }, { navigate, storage })).not.toThrow();
});
});
});
24 changes: 24 additions & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import BaseClient, { LogtoConfig, createRequester, ClientAdapter } from '@logto/client';
import fetch from 'node-fetch';

import { generateCodeChallenge, generateCodeVerifier, generateState } from './utils/generators';

export type {
IdTokenClaims,
LogtoErrorCode,
LogtoConfig,
LogtoClientErrorCode,
} from '@logto/client';
export { LogtoError, OidcError, Prompt, LogtoRequestError, LogtoClientError } from '@logto/client';

export default class LogtoClient extends BaseClient {
constructor(config: LogtoConfig, adapter: Pick<ClientAdapter, 'navigate' | 'storage'>) {
super(config, {
...adapter,
requester: createRequester(fetch),
generateCodeChallenge,
generateCodeVerifier,
generateState,
});
}
}
80 changes: 80 additions & 0 deletions packages/node/src/utils/generators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { UrlSafeBase64 } from '@silverhand/essentials';
import { toUint8Array } from 'js-base64';

import { generateCodeChallenge, generateCodeVerifier, generateState } from './generators';

describe('generateState', () => {
test('should be random value', () => {
const state1 = generateState();
const state2 = generateState();
expect(state1).not.toEqual(state2);
});

test('should be url-safe', () => {
const state = generateState();
expect(UrlSafeBase64.isSafe(state)).toBeTruthy();
});

test('raw random data length should be length 64', () => {
const state = generateState();
expect(toUint8Array(state).length).toEqual(64);
});
});

describe('generateCodeVerifier', () => {
test('should be random value', () => {
const codeVerifier1 = generateCodeVerifier();
const codeVerifier2 = generateCodeVerifier();
expect(codeVerifier1).not.toEqual(codeVerifier2);
});

test('should be url-safe', () => {
const codeVerifier = generateCodeVerifier();
expect(UrlSafeBase64.isSafe(codeVerifier)).toBeTruthy();
});

test('raw random data length should be length 64', () => {
const codeVerifier = generateCodeVerifier();
expect(toUint8Array(codeVerifier).length).toEqual(64);
});
});

describe('generateCodeChallenge', () => {
test('dealing with different code verifiers should not be equal', async () => {
const codeVerifier1 = generateCodeVerifier();
const codeChallenge1 = await generateCodeChallenge(codeVerifier1);
const codeVerifier2 = generateCodeVerifier();
const codeChallenge2 = await generateCodeChallenge(codeVerifier2);
expect(codeChallenge1).not.toEqual(codeChallenge2);
});

test('dealing with same code verifier should be equal', async () => {
const codeVerifier = generateCodeVerifier();
const codeChallenge1 = await generateCodeChallenge(codeVerifier);
const codeChallenge2 = await generateCodeChallenge(codeVerifier);
expect(codeChallenge1).toEqual(codeChallenge2);
});

describe('dealing with static code verifier should not throw', () => {
test('dealing with url-safe code verifier should not throw', async () => {
const codeVerifier =
'tO6MabnMFRAatnlMa1DdSstypzzkgalL1-k8Hr_GdfTj-VXGiEACqAkSkDhFuAuD8FOU8lMishaXjt29Xt2Oww';
const codeChallenge = await generateCodeChallenge(codeVerifier);
expect(codeChallenge).toEqual('0K3SLeGlNNzFswYJjcVzcN4C76m_8NZORxFJLBJWGwg');
});

describe('dealing with non-url-safe code verifier should not throw', () => {
test('latin1 character', async () => {
const codeVerifier = 'Á';
const codeChallenge = await generateCodeChallenge(codeVerifier);
expect(codeChallenge).toEqual('p3yvZiKYauPicLIDZ0W1peDz4Z9KFC-9uxtDfoO1KOQ');
});

test('emoji character', async () => {
const codeVerifier = '🚀';
const codeChallenge = await generateCodeChallenge(codeVerifier);
expect(codeChallenge).toEqual('67wLKHDrMj8rbP-lxJPO74GufrNq_HPU4DZzAWMdrsU');
});
});
});
});
37 changes: 37 additions & 0 deletions packages/node/src/utils/generators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/** @link [Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636) */
import { randomFillSync, createHash } from 'crypto';

import { fromUint8Array } from 'js-base64';

/**
* @param length The length of the raw random data.
*/
const generateRandomString = (length = 64) =>
fromUint8Array(randomFillSync(new Uint8Array(length)), true);

/**
* Generates random string for state and encodes them in url safe base64
*/
export const generateState = () => generateRandomString();

/**
* Generates code verifier
*
* @link [Client Creates a Code Verifier](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1)
*/
export const generateCodeVerifier = () => generateRandomString();

/**
* Calculates the S256 PKCE code challenge for an arbitrary code verifier and encodes it in url safe base64
*
* @param {String} codeVerifier Code verifier to calculate the S256 code challenge for
* @link [Client Creates the Code Challenge](https://datatracker.ietf.org/doc/html/rfc7636#section-4.2)
*/
export const generateCodeChallenge = async (codeVerifier: string): Promise<string> => {
const encodedCodeVerifier = new TextEncoder().encode(codeVerifier);
const hash = createHash('sha256');
hash.update(encodedCodeVerifier);
const codeChallenge = hash.digest();

return fromUint8Array(codeChallenge, true);
};
14 changes: 14 additions & 0 deletions packages/node/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "@silverhand/ts-config/tsconfig.base",
"compilerOptions": {
"outDir": "lib",
"types": [
"jest",
"jest-matcher-specific-error"
]
},
"include": [
"src",
"jest.config.ts",
]
}
62 changes: 56 additions & 6 deletions pnpm-lock.yaml

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

0 comments on commit 2cb03c1

Please sign in to comment.