Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
ee7a2ab
step1-split wallet and auth
shineli1984 Nov 18, 2025
f164052
add lock file
shineli1984 Nov 18, 2025
f065702
make sure the code are copied
shineli1984 Nov 24, 2025
6958697
syncpack
shineli1984 Nov 24, 2025
fac8e9d
pnpm lock file
shineli1984 Nov 24, 2025
88a8058
node types
shineli1984 Nov 24, 2025
1b2aa6d
remove as any
shineli1984 Nov 24, 2025
9e5e0a5
wallet build
shineli1984 Nov 24, 2025
3bc1ba8
sample app eslint fix
shineli1984 Nov 24, 2025
798597f
add missing dependency
shineli1984 Nov 24, 2025
6b87732
remove unnecessary files
shineli1984 Nov 24, 2025
28eacbf
sample app lint
shineli1984 Nov 24, 2025
15bfd09
fix chainID
shineli1984 Nov 24, 2025
fb595ff
fix type error
shineli1984 Nov 24, 2025
9a19c4e
fix provider type
shineli1984 Nov 24, 2025
b101708
fix type
shineli1984 Nov 24, 2025
bcfb6a7
fix config
shineli1984 Nov 24, 2025
6780bd7
use authconfig insted
shineli1984 Nov 24, 2025
91461e6
Merge branch 'main' into v3alpha
shineli1984 Nov 24, 2025
73516a8
fix deployment settings
shineli1984 Nov 24, 2025
dc62012
fix publish settings
shineli1984 Nov 24, 2025
56b73dc
type module
shineli1984 Nov 25, 2025
8aeeeba
align package.json
shineli1984 Nov 25, 2025
648174a
make passport use auth and wallet
shineli1984 Nov 26, 2025
a5f2f97
fix build
shineli1984 Nov 26, 2025
bccc29a
session activity
shineli1984 Nov 26, 2025
48ae93f
remove anomynous id, move tracking
shineli1984 Nov 26, 2025
969c7b2
remove anomynous id
shineli1984 Nov 26, 2025
eb12c6f
fix session activity
shineli1984 Nov 27, 2025
f3324f5
simplify interface
shineli1984 Nov 27, 2025
dbdcf54
move imx stuff back to passport
shineli1984 Nov 27, 2025
a07d6a2
lock file
shineli1984 Nov 27, 2025
508a9e8
refactor
shineli1984 Nov 28, 2025
75b8c8a
default auth
shineli1984 Dec 1, 2025
5b06bd9
lint
shineli1984 Dec 1, 2025
f112217
sdk version
shineli1984 Dec 1, 2025
f6017c1
conflicts
shineli1984 Dec 1, 2025
e10715d
syncpack
shineli1984 Dec 1, 2025
b21f021
lock file
shineli1984 Dec 1, 2025
3453f0f
fix sample app
shineli1984 Dec 2, 2025
d71d3d0
error type fix
shineli1984 Dec 2, 2025
3df2460
add comments
shineli1984 Dec 2, 2025
4b17b25
fix build
shineli1984 Dec 2, 2025
d8f2850
add tests
shineli1984 Dec 2, 2025
a214737
fix test config
shineli1984 Dec 3, 2025
137050d
syncpack
shineli1984 Dec 3, 2025
5256255
login event
shineli1984 Dec 3, 2025
583b130
Merge branch 'main' into v3alpha
shineli1984 Dec 3, 2025
ca49c4e
conflicts
shineli1984 Dec 5, 2025
1798bc8
bug
shineli1984 Dec 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# prevent heap limit allocation errors
export NODE_OPTIONS="--max-old-space-size=4096"
# prevent heap limit allocation errors - increased to 8GB
export NODE_OPTIONS="--max-old-space-size=8192"

pnpm lint-staged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@swc-node/register": "^1.10.9",
"@swc/cli": "^0.4.0",
"@swc/core": "^1.3.36",
"@jest/test-sequencer": "^29.7.0",
"@types/chai": "^4.3.16",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
Expand Down
18 changes: 18 additions & 0 deletions packages/auth/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
extends: ['../../.eslintrc'],
parserOptions: {
project: './tsconfig.eslint.json',
tsconfigRootDir: __dirname,
},
rules: {
// Disable all import plugin rules due to stack overflow with auth package structure
'import/order': 'off',
'import/no-unresolved': 'off',
'import/named': 'off',
'import/default': 'off',
'import/namespace': 'off',
'import/no-cycle': 'off',
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
},
};
27 changes: 27 additions & 0 deletions packages/auth/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Config } from 'jest';
import { execSync } from 'child_process';
import { name } from './package.json';

const rootDirs = execSync(`pnpm --filter ${name}... exec pwd`)
.toString()
.split('\n')
.filter(Boolean)
.map((dir) => `${dir}/dist`);

const config: Config = {
clearMocks: true,
roots: ['<rootDir>/src', ...rootDirs],
coverageProvider: 'v8',
moduleDirectories: ['node_modules', 'src'],
moduleNameMapper: { '^@imtbl/(.*)$': '<rootDir>/../../node_modules/@imtbl/$1/src' },
testEnvironment: 'jsdom',
transform: {
'^.+\\.(t|j)sx?$': '@swc/jest',
},
transformIgnorePatterns: [],
restoreMocks: true,
setupFiles: ['<rootDir>/jest.setup.js'],
};

export default config;

4 changes: 4 additions & 0 deletions packages/auth/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { TextEncoder } from 'util';

global.TextEncoder = TextEncoder;

64 changes: 64 additions & 0 deletions packages/auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"name": "@imtbl/auth",
"version": "0.0.0",
"description": "Authentication SDK for Immutable",
"author": "Immutable",
"bugs": "https://github.com/immutable/ts-immutable-sdk/issues",
"homepage": "https://github.com/immutable/ts-immutable-sdk#readme",
"license": "Apache-2.0",
"main": "dist/node/index.cjs",
"module": "dist/node/index.js",
"browser": "dist/browser/index.js",
"types": "./dist/types/index.d.ts",
"exports": {
"development": {
"types": "./src/index.ts",
"browser": "./dist/browser/index.js",
"require": "./dist/node/index.cjs",
"default": "./dist/node/index.js"
},
"default": {
"types": "./dist/types/index.d.ts",
"browser": "./dist/browser/index.js",
"require": "./dist/node/index.cjs",
"default": "./dist/node/index.js"
}
},
"scripts": {
"build": "pnpm transpile && pnpm typegen",
"transpile": "tsup src/index.ts --config ../../tsup.config.js",
"typegen": "tsc --customConditions default --emitDeclarationOnly --outDir dist/types",
"pack:root": "pnpm pack --pack-destination $(dirname $(pnpm root -w))",
"lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0",
"typecheck": "tsc --customConditions default --noEmit --jsx preserve",
"test": "jest"
},
"dependencies": {
"@imtbl/config": "workspace:*",
"@imtbl/metrics": "workspace:*",
"axios": "^1.6.5",
"jwt-decode": "^3.1.2",
"localforage": "^1.10.0",
"oidc-client-ts": "3.4.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@imtbl/toolkit": "workspace:*",
"@swc/core": "^1.3.36",
"@swc/jest": "^0.2.37",
"@types/jest": "^29.5.12",
"@types/node": "^18.14.2",
"@jest/test-sequencer": "^29.7.0",
"jest": "^29.4.3",
"jest-environment-jsdom": "^29.4.3",
"ts-node": "^10.9.1",
"tsup": "^8.3.0",
"typescript": "^5.6.2"
},
"publishConfig": {
"access": "public"
},
"repository": "immutable/ts-immutable-sdk.git",
"type": "module"
}

186 changes: 186 additions & 0 deletions packages/auth/src/Auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { Auth } from './Auth';
import { AuthEvents, User } from './types';
import { withMetricsAsync } from './utils/metrics';
import jwt_decode from 'jwt-decode';

const trackFlowMock = jest.fn();
const trackErrorMock = jest.fn();
const identifyMock = jest.fn();
const trackMock = jest.fn();
const getDetailMock = jest.fn();

jest.mock('@imtbl/metrics', () => ({
Detail: { RUNTIME_ID: 'runtime-id' },
trackFlow: (...args: any[]) => trackFlowMock(...args),
trackError: (...args: any[]) => trackErrorMock(...args),
identify: (...args: any[]) => identifyMock(...args),
track: (...args: any[]) => trackMock(...args),
getDetail: (...args: any[]) => getDetailMock(...args),
}));

jest.mock('jwt-decode', () => jest.fn());

beforeEach(() => {
trackFlowMock.mockReset();
trackErrorMock.mockReset();
identifyMock.mockReset();
trackMock.mockReset();
getDetailMock.mockReset();
(jwt_decode as jest.Mock).mockReset();
});

describe('withMetricsAsync', () => {
it('resolves with function result and tracks flow', async () => {
const flow = {
addEvent: jest.fn(),
details: { flowId: 'flow-id' },
};
trackFlowMock.mockReturnValue(flow);

const result = await withMetricsAsync(async () => 'done', 'login');

expect(result).toEqual('done');
expect(trackFlowMock).toHaveBeenCalledWith('passport', 'login', true);
expect(flow.addEvent).toHaveBeenCalledWith('End');
});

it('tracks error when function throws', async () => {
const flow = {
addEvent: jest.fn(),
details: { flowId: 'flow-id' },
};
trackFlowMock.mockReturnValue(flow);
const error = new Error('boom');

await expect(withMetricsAsync(async () => {
throw error;
}, 'login')).rejects.toThrow(error);

expect(trackErrorMock).toHaveBeenCalledWith('passport', 'login', error, { flowId: 'flow-id' });
expect(flow.addEvent).toHaveBeenCalledWith('End');
});

it('does not fail when non-error is thrown', async () => {
const flow = {
addEvent: jest.fn(),
details: { flowId: 'flow-id' },
};
trackFlowMock.mockReturnValue(flow);

const nonError = { message: 'failure' };
await expect(withMetricsAsync(async () => {
throw nonError as unknown as Error;
}, 'login')).rejects.toBe(nonError);

expect(flow.addEvent).toHaveBeenCalledWith('errored');
});
});

describe('Auth', () => {
describe('getUserOrLogin', () => {
const createMockUser = (): User => ({
accessToken: 'access',
idToken: 'id',
refreshToken: 'refresh',
expired: false,
profile: {
sub: 'user-123',
email: 'test@example.com',
nickname: 'tester',
},
});

it('emits LOGGED_IN event and identifies user when login is required', async () => {
const auth = Object.create(Auth.prototype) as Auth;
const loginWithPopup = jest.fn().mockResolvedValue(createMockUser());

(auth as any).eventEmitter = { emit: jest.fn() };
(auth as any).getUserInternal = jest.fn().mockResolvedValue(null);
(auth as any).loginWithPopup = loginWithPopup;

const user = await auth.getUserOrLogin();

expect(loginWithPopup).toHaveBeenCalledTimes(1);
expect((auth as any).eventEmitter.emit).toHaveBeenCalledWith(AuthEvents.LOGGED_IN, user);
expect(identifyMock).toHaveBeenCalledWith({ passportId: user.profile.sub });
});

it('returns cached user without triggering login', async () => {
const auth = Object.create(Auth.prototype) as Auth;
const cachedUser = createMockUser();

(auth as any).eventEmitter = { emit: jest.fn() };
(auth as any).getUserInternal = jest.fn().mockResolvedValue(cachedUser);
(auth as any).loginWithPopup = jest.fn();

const user = await auth.getUserOrLogin();

expect(user).toBe(cachedUser);
expect((auth as any).loginWithPopup).not.toHaveBeenCalled();
expect((auth as any).eventEmitter.emit).not.toHaveBeenCalled();
expect(identifyMock).not.toHaveBeenCalled();
});
});

describe('buildExtraQueryParams', () => {
it('omits third_party_a_id when no anonymous id is provided', () => {
const auth = Object.create(Auth.prototype) as Auth;
(auth as any).userManager = { settings: { extraQueryParams: {} } };
getDetailMock.mockReturnValue('runtime-id-value');

const params = (auth as any).buildExtraQueryParams();

expect(params.third_party_a_id).toBeUndefined();
expect(params.rid).toEqual('runtime-id-value');
});
});

describe('username extraction', () => {
it('extracts username from id token when present', () => {
const mockOidcUser = {
id_token: 'token',
access_token: 'access',
refresh_token: 'refresh',
expired: false,
profile: { sub: 'user-123', email: 'test@example.com', nickname: 'tester' },
};

(jwt_decode as jest.Mock).mockReturnValue({
username: 'username123',
passport: undefined,
});

const result = (Auth as any).mapOidcUserToDomainModel(mockOidcUser);

expect(jwt_decode).toHaveBeenCalledWith('token');
expect(result.profile.username).toEqual('username123');
});

it('maps username when creating OIDC user from device tokens', () => {
const tokenResponse = {
id_token: 'token',
access_token: 'access',
refresh_token: 'refresh',
token_type: 'Bearer',
expires_in: 3600,
};

(jwt_decode as jest.Mock).mockReturnValue({
sub: 'user-123',
iss: 'issuer',
aud: 'audience',
exp: 1,
iat: 0,
email: 'test@example.com',
nickname: 'tester',
username: 'username123',
passport: undefined,
});

const oidcUser = (Auth as any).mapDeviceTokenResponseToOidcUser(tokenResponse);

expect(jwt_decode).toHaveBeenCalledWith('token');
expect(oidcUser.profile.username).toEqual('username123');
});
});
});
Loading