@tenxyte/core 0.1.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -0
- package/dist/index.cjs +951 -496
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1709 -1265
- package/dist/index.d.ts +1709 -1265
- package/dist/index.js +919 -464
- package/dist/index.js.map +1 -1
- package/package.json +70 -66
- package/src/client.ts +50 -21
- package/src/http/client.ts +162 -162
- package/src/http/index.ts +1 -1
- package/src/http/interceptors.ts +117 -117
- package/src/index.ts +7 -7
- package/src/modules/ai.ts +178 -0
- package/src/modules/auth.ts +116 -95
- package/src/modules/b2b.ts +177 -0
- package/src/modules/rbac.ts +207 -160
- package/src/modules/security.ts +313 -122
- package/src/modules/user.ts +95 -80
- package/src/storage/cookie.ts +39 -39
- package/src/storage/index.ts +29 -29
- package/src/storage/localStorage.ts +75 -75
- package/src/storage/memory.ts +30 -30
- package/src/types/index.ts +152 -150
- package/src/utils/base64url.ts +25 -0
- package/src/utils/device_info.ts +94 -94
- package/src/utils/events.ts +71 -71
- package/src/utils/jwt.ts +51 -51
- package/tests/http.test.ts +144 -144
- package/tests/modules/auth.test.ts +93 -93
- package/tests/modules/rbac.test.ts +95 -95
- package/tests/modules/security.test.ts +85 -75
- package/tests/modules/user.test.ts +76 -76
- package/tests/storage.test.ts +96 -96
- package/tests/utils.test.ts +71 -71
- package/tsup.config.ts +10 -10
- package/vitest.config.ts +7 -7
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function bufferToBase64url(buffer: ArrayBuffer | Uint8Array): string {
|
|
2
|
+
const bytes = new Uint8Array(buffer);
|
|
3
|
+
let binary = '';
|
|
4
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
5
|
+
binary += String.fromCharCode(bytes[i]);
|
|
6
|
+
}
|
|
7
|
+
return btoa(binary)
|
|
8
|
+
.replace(/\+/g, '-')
|
|
9
|
+
.replace(/\//g, '_')
|
|
10
|
+
.replace(/=/g, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function base64urlToBuffer(base64url: string): Uint8Array {
|
|
14
|
+
const base64 = base64url
|
|
15
|
+
.replace(/-/g, '+')
|
|
16
|
+
.replace(/_/g, '/');
|
|
17
|
+
const padLen = (4 - (base64.length % 4)) % 4;
|
|
18
|
+
const padded = base64 + '='.repeat(padLen);
|
|
19
|
+
const binary = atob(padded);
|
|
20
|
+
const bytes = new Uint8Array(binary.length);
|
|
21
|
+
for (let i = 0; i < binary.length; i++) {
|
|
22
|
+
bytes[i] = binary.charCodeAt(i);
|
|
23
|
+
}
|
|
24
|
+
return bytes;
|
|
25
|
+
}
|
package/src/utils/device_info.ts
CHANGED
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Helper utility to build the device fingerprint required by Tenxyte security features.
|
|
3
|
-
* Format: `v=1|os=windows;osv=11|device=desktop|arch=x64|app=tenxyte;appv=1.0.0|runtime=chrome;rtv=122|tz=Europe/Paris`
|
|
4
|
-
*/
|
|
5
|
-
export interface CustomDeviceInfo {
|
|
6
|
-
os?: string;
|
|
7
|
-
osVersion?: string;
|
|
8
|
-
device?: string;
|
|
9
|
-
arch?: string;
|
|
10
|
-
app?: string;
|
|
11
|
-
appVersion?: string;
|
|
12
|
-
runtime?: string;
|
|
13
|
-
runtimeVersion?: string;
|
|
14
|
-
timezone?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function buildDeviceInfo(customInfo: CustomDeviceInfo = {}): string {
|
|
18
|
-
// Try to determine automatically from navigator
|
|
19
|
-
const autoInfo = getAutoInfo();
|
|
20
|
-
|
|
21
|
-
const v = '1';
|
|
22
|
-
const os = customInfo.os || autoInfo.os;
|
|
23
|
-
const osv = customInfo.osVersion || autoInfo.osVersion;
|
|
24
|
-
const device = customInfo.device || autoInfo.device;
|
|
25
|
-
const arch = customInfo.arch || autoInfo.arch;
|
|
26
|
-
const app = customInfo.app || autoInfo.app;
|
|
27
|
-
const appv = customInfo.appVersion || autoInfo.appVersion;
|
|
28
|
-
const runtime = customInfo.runtime || autoInfo.runtime;
|
|
29
|
-
const rtv = customInfo.runtimeVersion || autoInfo.runtimeVersion;
|
|
30
|
-
const tz = customInfo.timezone || autoInfo.timezone;
|
|
31
|
-
|
|
32
|
-
const parts = [
|
|
33
|
-
`v=${v}`,
|
|
34
|
-
`os=${os}` + (osv ? `;osv=${osv}` : ''),
|
|
35
|
-
`device=${device}`,
|
|
36
|
-
arch ? `arch=${arch}` : '',
|
|
37
|
-
app ? `app=${app}${appv ? `;appv=${appv}` : ''}` : '',
|
|
38
|
-
`runtime=${runtime}` + (rtv ? `;rtv=${rtv}` : ''),
|
|
39
|
-
tz ? `tz=${tz}` : ''
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
return parts.filter(Boolean).join('|');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function getAutoInfo() {
|
|
46
|
-
const info = {
|
|
47
|
-
os: 'unknown',
|
|
48
|
-
osVersion: '',
|
|
49
|
-
device: 'desktop', // default
|
|
50
|
-
arch: '',
|
|
51
|
-
app: 'sdk',
|
|
52
|
-
appVersion: '0.1.0',
|
|
53
|
-
runtime: 'unknown',
|
|
54
|
-
runtimeVersion: '',
|
|
55
|
-
timezone: ''
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
if (typeof Intl !== 'undefined') {
|
|
60
|
-
info.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (typeof process !== 'undefined' && process.version) {
|
|
64
|
-
info.runtime = 'node';
|
|
65
|
-
info.runtimeVersion = process.version;
|
|
66
|
-
info.os = process.platform;
|
|
67
|
-
info.arch = process.arch;
|
|
68
|
-
info.device = 'server';
|
|
69
|
-
} else if (typeof window !== 'undefined' && window.navigator) {
|
|
70
|
-
const ua = window.navigator.userAgent.toLowerCase();
|
|
71
|
-
|
|
72
|
-
// Basic OS detection
|
|
73
|
-
if (ua.includes('windows')) info.os = 'windows';
|
|
74
|
-
else if (ua.includes('mac')) info.os = 'macos';
|
|
75
|
-
else if (ua.includes('linux')) info.os = 'linux';
|
|
76
|
-
else if (ua.includes('android')) info.os = 'android';
|
|
77
|
-
else if (ua.includes('ios') || ua.includes('iphone') || ua.includes('ipad')) info.os = 'ios';
|
|
78
|
-
|
|
79
|
-
// Basic Device Type
|
|
80
|
-
if (/mobi|android|touch|mini/i.test(ua)) info.device = 'mobile';
|
|
81
|
-
if (/tablet|ipad/i.test(ua)) info.device = 'tablet';
|
|
82
|
-
|
|
83
|
-
// Basic Runtime (Browser)
|
|
84
|
-
if (ua.includes('firefox')) info.runtime = 'firefox';
|
|
85
|
-
else if (ua.includes('edg/')) info.runtime = 'edge';
|
|
86
|
-
else if (ua.includes('chrome')) info.runtime = 'chrome';
|
|
87
|
-
else if (ua.includes('safari')) info.runtime = 'safari';
|
|
88
|
-
}
|
|
89
|
-
} catch (e) {
|
|
90
|
-
// Ignore context extraction errors
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return info;
|
|
94
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Helper utility to build the device fingerprint required by Tenxyte security features.
|
|
3
|
+
* Format: `v=1|os=windows;osv=11|device=desktop|arch=x64|app=tenxyte;appv=1.0.0|runtime=chrome;rtv=122|tz=Europe/Paris`
|
|
4
|
+
*/
|
|
5
|
+
export interface CustomDeviceInfo {
|
|
6
|
+
os?: string;
|
|
7
|
+
osVersion?: string;
|
|
8
|
+
device?: string;
|
|
9
|
+
arch?: string;
|
|
10
|
+
app?: string;
|
|
11
|
+
appVersion?: string;
|
|
12
|
+
runtime?: string;
|
|
13
|
+
runtimeVersion?: string;
|
|
14
|
+
timezone?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildDeviceInfo(customInfo: CustomDeviceInfo = {}): string {
|
|
18
|
+
// Try to determine automatically from navigator
|
|
19
|
+
const autoInfo = getAutoInfo();
|
|
20
|
+
|
|
21
|
+
const v = '1';
|
|
22
|
+
const os = customInfo.os || autoInfo.os;
|
|
23
|
+
const osv = customInfo.osVersion || autoInfo.osVersion;
|
|
24
|
+
const device = customInfo.device || autoInfo.device;
|
|
25
|
+
const arch = customInfo.arch || autoInfo.arch;
|
|
26
|
+
const app = customInfo.app || autoInfo.app;
|
|
27
|
+
const appv = customInfo.appVersion || autoInfo.appVersion;
|
|
28
|
+
const runtime = customInfo.runtime || autoInfo.runtime;
|
|
29
|
+
const rtv = customInfo.runtimeVersion || autoInfo.runtimeVersion;
|
|
30
|
+
const tz = customInfo.timezone || autoInfo.timezone;
|
|
31
|
+
|
|
32
|
+
const parts = [
|
|
33
|
+
`v=${v}`,
|
|
34
|
+
`os=${os}` + (osv ? `;osv=${osv}` : ''),
|
|
35
|
+
`device=${device}`,
|
|
36
|
+
arch ? `arch=${arch}` : '',
|
|
37
|
+
app ? `app=${app}${appv ? `;appv=${appv}` : ''}` : '',
|
|
38
|
+
`runtime=${runtime}` + (rtv ? `;rtv=${rtv}` : ''),
|
|
39
|
+
tz ? `tz=${tz}` : ''
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
return parts.filter(Boolean).join('|');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getAutoInfo() {
|
|
46
|
+
const info = {
|
|
47
|
+
os: 'unknown',
|
|
48
|
+
osVersion: '',
|
|
49
|
+
device: 'desktop', // default
|
|
50
|
+
arch: '',
|
|
51
|
+
app: 'sdk',
|
|
52
|
+
appVersion: '0.1.0',
|
|
53
|
+
runtime: 'unknown',
|
|
54
|
+
runtimeVersion: '',
|
|
55
|
+
timezone: ''
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
if (typeof Intl !== 'undefined') {
|
|
60
|
+
info.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof process !== 'undefined' && process.version) {
|
|
64
|
+
info.runtime = 'node';
|
|
65
|
+
info.runtimeVersion = process.version;
|
|
66
|
+
info.os = process.platform;
|
|
67
|
+
info.arch = process.arch;
|
|
68
|
+
info.device = 'server';
|
|
69
|
+
} else if (typeof window !== 'undefined' && window.navigator) {
|
|
70
|
+
const ua = window.navigator.userAgent.toLowerCase();
|
|
71
|
+
|
|
72
|
+
// Basic OS detection
|
|
73
|
+
if (ua.includes('windows')) info.os = 'windows';
|
|
74
|
+
else if (ua.includes('mac')) info.os = 'macos';
|
|
75
|
+
else if (ua.includes('linux')) info.os = 'linux';
|
|
76
|
+
else if (ua.includes('android')) info.os = 'android';
|
|
77
|
+
else if (ua.includes('ios') || ua.includes('iphone') || ua.includes('ipad')) info.os = 'ios';
|
|
78
|
+
|
|
79
|
+
// Basic Device Type
|
|
80
|
+
if (/mobi|android|touch|mini/i.test(ua)) info.device = 'mobile';
|
|
81
|
+
if (/tablet|ipad/i.test(ua)) info.device = 'tablet';
|
|
82
|
+
|
|
83
|
+
// Basic Runtime (Browser)
|
|
84
|
+
if (ua.includes('firefox')) info.runtime = 'firefox';
|
|
85
|
+
else if (ua.includes('edg/')) info.runtime = 'edge';
|
|
86
|
+
else if (ua.includes('chrome')) info.runtime = 'chrome';
|
|
87
|
+
else if (ua.includes('safari')) info.runtime = 'safari';
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Ignore context extraction errors
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return info;
|
|
94
|
+
}
|
package/src/utils/events.ts
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight EventEmitter for TenxyteClient.
|
|
3
|
-
* Provides `.on`, `.once`, `.off`, and `.emit`.
|
|
4
|
-
*/
|
|
5
|
-
export class EventEmitter<Events extends Record<string, any>> {
|
|
6
|
-
private events: Map<keyof Events, Array<Function>>;
|
|
7
|
-
|
|
8
|
-
constructor() {
|
|
9
|
-
this.events = new Map();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Subscribe to an event.
|
|
14
|
-
* @param event The event name
|
|
15
|
-
* @param callback The callback function
|
|
16
|
-
* @returns Unsubscribe function
|
|
17
|
-
*/
|
|
18
|
-
on<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): () => void {
|
|
19
|
-
if (!this.events.has(event)) {
|
|
20
|
-
this.events.set(event, []);
|
|
21
|
-
}
|
|
22
|
-
this.events.get(event)!.push(callback);
|
|
23
|
-
return () => this.off(event, callback);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Unsubscribe from an event.
|
|
28
|
-
* @param event The event name
|
|
29
|
-
* @param callback The exact callback function that was passed to .on()
|
|
30
|
-
*/
|
|
31
|
-
off<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): void {
|
|
32
|
-
const callbacks = this.events.get(event);
|
|
33
|
-
if (!callbacks) return;
|
|
34
|
-
const index = callbacks.indexOf(callback);
|
|
35
|
-
if (index !== -1) {
|
|
36
|
-
callbacks.splice(index, 1);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Subscribe to an event exactly once.
|
|
42
|
-
*/
|
|
43
|
-
once<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): () => void {
|
|
44
|
-
const wrapped = (payload: Events[K]) => {
|
|
45
|
-
this.off(event, wrapped);
|
|
46
|
-
callback(payload);
|
|
47
|
-
};
|
|
48
|
-
return this.on(event, wrapped);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Emit an event internally.
|
|
53
|
-
*/
|
|
54
|
-
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
|
|
55
|
-
const callbacks = this.events.get(event);
|
|
56
|
-
if (!callbacks) return;
|
|
57
|
-
// Copy array to prevent mutation issues during emission
|
|
58
|
-
const copy = [...callbacks];
|
|
59
|
-
for (const callback of copy) {
|
|
60
|
-
try {
|
|
61
|
-
callback(payload);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
console.error(`[Tenxyte EventEmitter] Error executing callback for event ${String(event)}`, err);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
removeAllListeners(): void {
|
|
69
|
-
this.events.clear();
|
|
70
|
-
}
|
|
71
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight EventEmitter for TenxyteClient.
|
|
3
|
+
* Provides `.on`, `.once`, `.off`, and `.emit`.
|
|
4
|
+
*/
|
|
5
|
+
export class EventEmitter<Events extends Record<string, any>> {
|
|
6
|
+
private events: Map<keyof Events, Array<Function>>;
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
this.events = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Subscribe to an event.
|
|
14
|
+
* @param event The event name
|
|
15
|
+
* @param callback The callback function
|
|
16
|
+
* @returns Unsubscribe function
|
|
17
|
+
*/
|
|
18
|
+
on<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): () => void {
|
|
19
|
+
if (!this.events.has(event)) {
|
|
20
|
+
this.events.set(event, []);
|
|
21
|
+
}
|
|
22
|
+
this.events.get(event)!.push(callback);
|
|
23
|
+
return () => this.off(event, callback);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Unsubscribe from an event.
|
|
28
|
+
* @param event The event name
|
|
29
|
+
* @param callback The exact callback function that was passed to .on()
|
|
30
|
+
*/
|
|
31
|
+
off<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): void {
|
|
32
|
+
const callbacks = this.events.get(event);
|
|
33
|
+
if (!callbacks) return;
|
|
34
|
+
const index = callbacks.indexOf(callback);
|
|
35
|
+
if (index !== -1) {
|
|
36
|
+
callbacks.splice(index, 1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Subscribe to an event exactly once.
|
|
42
|
+
*/
|
|
43
|
+
once<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): () => void {
|
|
44
|
+
const wrapped = (payload: Events[K]) => {
|
|
45
|
+
this.off(event, wrapped);
|
|
46
|
+
callback(payload);
|
|
47
|
+
};
|
|
48
|
+
return this.on(event, wrapped);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Emit an event internally.
|
|
53
|
+
*/
|
|
54
|
+
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
|
|
55
|
+
const callbacks = this.events.get(event);
|
|
56
|
+
if (!callbacks) return;
|
|
57
|
+
// Copy array to prevent mutation issues during emission
|
|
58
|
+
const copy = [...callbacks];
|
|
59
|
+
for (const callback of copy) {
|
|
60
|
+
try {
|
|
61
|
+
callback(payload);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error(`[Tenxyte EventEmitter] Error executing callback for event ${String(event)}`, err);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
removeAllListeners(): void {
|
|
69
|
+
this.events.clear();
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/utils/jwt.ts
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
export interface DecodedTenxyteToken {
|
|
2
|
-
exp?: number;
|
|
3
|
-
iat?: number;
|
|
4
|
-
sub?: string;
|
|
5
|
-
roles?: string[];
|
|
6
|
-
permissions?: string[];
|
|
7
|
-
[key: string]: any;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Decodes the payload of a JWT without verifying the signature.
|
|
12
|
-
* Suitable for client-side routing and UI state.
|
|
13
|
-
*/
|
|
14
|
-
export function decodeJwt(token: string): DecodedTenxyteToken | null {
|
|
15
|
-
try {
|
|
16
|
-
const parts = token.split('.');
|
|
17
|
-
if (parts.length !== 3) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let base64Url = parts[1];
|
|
22
|
-
if (!base64Url) return null;
|
|
23
|
-
|
|
24
|
-
let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
25
|
-
|
|
26
|
-
// Pad with standard base64 padding
|
|
27
|
-
while (base64.length % 4) {
|
|
28
|
-
base64 += '=';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const isBrowser = typeof window !== 'undefined' && typeof window.atob === 'function';
|
|
32
|
-
let jsonPayload: string;
|
|
33
|
-
|
|
34
|
-
if (isBrowser) {
|
|
35
|
-
// Browser decode
|
|
36
|
-
jsonPayload = decodeURIComponent(
|
|
37
|
-
window.atob(base64)
|
|
38
|
-
.split('')
|
|
39
|
-
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
|
40
|
-
.join('')
|
|
41
|
-
);
|
|
42
|
-
} else {
|
|
43
|
-
// Node.js decode
|
|
44
|
-
jsonPayload = Buffer.from(base64, 'base64').toString('utf8');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return JSON.parse(jsonPayload);
|
|
48
|
-
} catch (e) {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
1
|
+
export interface DecodedTenxyteToken {
|
|
2
|
+
exp?: number;
|
|
3
|
+
iat?: number;
|
|
4
|
+
sub?: string;
|
|
5
|
+
roles?: string[];
|
|
6
|
+
permissions?: string[];
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Decodes the payload of a JWT without verifying the signature.
|
|
12
|
+
* Suitable for client-side routing and UI state.
|
|
13
|
+
*/
|
|
14
|
+
export function decodeJwt(token: string): DecodedTenxyteToken | null {
|
|
15
|
+
try {
|
|
16
|
+
const parts = token.split('.');
|
|
17
|
+
if (parts.length !== 3) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let base64Url = parts[1];
|
|
22
|
+
if (!base64Url) return null;
|
|
23
|
+
|
|
24
|
+
let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
25
|
+
|
|
26
|
+
// Pad with standard base64 padding
|
|
27
|
+
while (base64.length % 4) {
|
|
28
|
+
base64 += '=';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isBrowser = typeof window !== 'undefined' && typeof window.atob === 'function';
|
|
32
|
+
let jsonPayload: string;
|
|
33
|
+
|
|
34
|
+
if (isBrowser) {
|
|
35
|
+
// Browser decode
|
|
36
|
+
jsonPayload = decodeURIComponent(
|
|
37
|
+
window.atob(base64)
|
|
38
|
+
.split('')
|
|
39
|
+
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
|
40
|
+
.join('')
|
|
41
|
+
);
|
|
42
|
+
} else {
|
|
43
|
+
// Node.js decode
|
|
44
|
+
jsonPayload = Buffer.from(base64, 'base64').toString('utf8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return JSON.parse(jsonPayload);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|