kerliix-oauth 1.0.1
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 +149 -0
- package/changelog.md +78 -0
- package/dist/cache.d.ts +7 -0
- package/dist/cache.js +23 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +116 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +1 -0
- package/package.json +31 -0
- package/src/cache.ts +28 -0
- package/src/client.ts +132 -0
- package/src/index.ts +3 -0
- package/src/types.ts +23 -0
- package/tests/client.test.js +12 -0
- package/tsconfig.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Kerliix OAuth SDK
|
|
2
|
+
|
|
3
|
+
> Official Kerliix OAuth 2.0 SDK for Node.js and Browser
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/kerliix-oauth)
|
|
6
|
+
[](https://github.com/kerliix-corp/kerliix-oauth-nodejs/blob/main/LICENSE)
|
|
7
|
+
[](https://github.com/kerliix-corp/kerliix-oauth-nodejs/actions)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
**Kerliix OAuth** provides a simple and secure way to integrate Kerliix authentication into your Node.js or browser-based apps.
|
|
14
|
+
It handles the full OAuth 2.0 flow including:
|
|
15
|
+
|
|
16
|
+
* Building authorization URLs
|
|
17
|
+
* Exchanging authorization codes for tokens
|
|
18
|
+
* Token caching & auto-refresh
|
|
19
|
+
* Fetching user profile data
|
|
20
|
+
* Revoking tokens
|
|
21
|
+
|
|
22
|
+
Built with **TypeScript**, compatible with both **Node.js** and **browser environments**.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install kerliix-oauth
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
or using yarn:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
yarn add kerliix-oauth
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import KerliixOAuth from "kerliix-oauth";
|
|
44
|
+
|
|
45
|
+
const client = new KerliixOAuth({
|
|
46
|
+
clientId: "YOUR_CLIENT_ID",
|
|
47
|
+
clientSecret: "YOUR_CLIENT_SECRET",
|
|
48
|
+
redirectUri: "https://yourapp.com/callback",
|
|
49
|
+
baseUrl: "https://api.kerliix.com"
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Step 1: Redirect user to login
|
|
53
|
+
const authUrl = client.getAuthUrl(["openid", "profile", "email"]);
|
|
54
|
+
console.log("Login via:", authUrl);
|
|
55
|
+
|
|
56
|
+
// Step 2: Exchange the code for tokens
|
|
57
|
+
// const token = await client.exchangeCodeForToken("OAUTH_CODE");
|
|
58
|
+
|
|
59
|
+
// Step 3: Fetch user profile
|
|
60
|
+
// const user = await client.getUserInfo(token.access_token);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Configuration Options
|
|
66
|
+
|
|
67
|
+
| Option | Required | Description |
|
|
68
|
+
| -------------- | -------- | ------------------------------------------------------------ |
|
|
69
|
+
| `clientId` | ✅ | Your app’s client ID from Kerliix developer portal |
|
|
70
|
+
| `clientSecret` | ⚙️ | Required for server-side (authorization code flow) |
|
|
71
|
+
| `redirectUri` | ✅ | The callback URI registered in your app |
|
|
72
|
+
| `baseUrl` | ✅ | Your Kerliix OAuth base URL, e.g. `https://auth.kerliix.com` |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## OAuth Flow
|
|
77
|
+
|
|
78
|
+
1. **User clicks login** → Redirect to Kerliix via `getAuthUrl()`
|
|
79
|
+
2. **Kerliix authenticates** → Redirects back with `?code=XYZ`
|
|
80
|
+
3. **Your app exchanges code** → `exchangeCodeForToken(code)`
|
|
81
|
+
4. **Use token** → Access user data via `getUserInfo()`
|
|
82
|
+
5. **Optional** → Automatically refresh expired tokens
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## API Reference
|
|
87
|
+
|
|
88
|
+
### `new KerliixOAuth(config)`
|
|
89
|
+
|
|
90
|
+
Initializes the client.
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
const client = new KerliixOAuth({
|
|
94
|
+
clientId: "...",
|
|
95
|
+
clientSecret: "...",
|
|
96
|
+
redirectUri: "...",
|
|
97
|
+
baseUrl: "https://api.kerliix.com"
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### `getAuthUrl(scopes?: string[], state?: string): string`
|
|
104
|
+
|
|
105
|
+
Generates an authorization URL for login/consent.
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const url = client.getAuthUrl(["openid", "profile", "email"], "xyz123");
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### `exchangeCodeForToken(code: string): Promise<TokenResponse>`
|
|
114
|
+
|
|
115
|
+
Exchanges an authorization code for access and refresh tokens.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### `refreshTokenIfNeeded(): Promise<TokenResponse | null>`
|
|
120
|
+
|
|
121
|
+
Checks if the token is expiring and automatically refreshes it if needed.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### `getUserInfo(accessToken?: string): Promise<UserInfo>`
|
|
126
|
+
|
|
127
|
+
Fetches the user’s profile data using a valid access token.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### `revokeToken(token: string): Promise<boolean>`
|
|
132
|
+
|
|
133
|
+
Revokes an access or refresh token.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Token Caching
|
|
138
|
+
|
|
139
|
+
The SDK automatically caches tokens in memory and:
|
|
140
|
+
|
|
141
|
+
* Refreshes them **30 seconds before expiry**
|
|
142
|
+
* Clears cache when tokens are revoked
|
|
143
|
+
* Works seamlessly in Node.js and browser environments
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT © Kerliix Corporation
|
package/changelog.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Quick Start / Known Issues (Local Development)
|
|
11
|
+
- **TypeScript syntax cannot run directly in Node.js** without compiling. Use `tsc` to build before running.
|
|
12
|
+
- **Do not use `!` non-null assertions** in runtime code (`CLIENT_ID!`) – they are TypeScript-only and will break Node.js execution.
|
|
13
|
+
- Exported functions with TypeScript types (e.g., `code: string`) will throw syntax errors if run uncompiled.
|
|
14
|
+
- Always handle runtime errors from SDK methods:
|
|
15
|
+
- `"Missing required config: clientId, redirectUri, or baseUrl"`
|
|
16
|
+
- `"Token exchange failed: <statusText>"`
|
|
17
|
+
- `"Failed to refresh token"`
|
|
18
|
+
- `"Missing access token"`
|
|
19
|
+
- `"Failed to fetch user info"`
|
|
20
|
+
- `revokeToken()` may return `false` if revocation fails.
|
|
21
|
+
- Use compiled `dist/` files for testing locally instead of raw `.ts` files.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## [1.0.1] - 2025-10-27
|
|
26
|
+
### Added
|
|
27
|
+
- **Runtime error documentation** for the SDK:
|
|
28
|
+
- `"Missing required config: clientId, redirectUri, or baseUrl"` in `KerliixOAuth` constructor.
|
|
29
|
+
- `"Token exchange failed: <statusText>"` in `exchangeCodeForToken()`.
|
|
30
|
+
- `"Failed to refresh token"` in `refreshTokenIfNeeded()`.
|
|
31
|
+
- `null` returned from `refreshTokenIfNeeded()` or `TokenCache.get()` if no valid token exists.
|
|
32
|
+
- `"Missing access token"` in `getUserInfo()` if no access token is available.
|
|
33
|
+
- `"Failed to fetch user info"` if user info request fails.
|
|
34
|
+
- `revokeToken()` returns `false` if token revocation fails.
|
|
35
|
+
- Enhanced **developer experience** with clearer SDK error messages and handling instructions.
|
|
36
|
+
- Minor improvements to TypeScript typings and internal caching logic.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- SDK version bumped from `1.0.0-beta.1` to `1.0.1`.
|
|
40
|
+
- Documentation and examples updated to highlight error handling.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- N/A (no functional changes; only runtime errors clarified).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## [1.0.0-beta.1] - 2025-10-15
|
|
48
|
+
### Added
|
|
49
|
+
- **Initial beta release** of the official **Kerliix OAuth SDK** for Node.js and browser.
|
|
50
|
+
- Support for **OAuth 2.0 Authorization Code Flow**.
|
|
51
|
+
- `KerliixOAuth` main client class for authentication and token management.
|
|
52
|
+
- `getAuthUrl()` method to generate authorization URLs.
|
|
53
|
+
- `exchangeCodeForToken()` to exchange authorization codes for tokens.
|
|
54
|
+
- `refreshTokenIfNeeded()` to refresh tokens automatically.
|
|
55
|
+
- `getUserInfo()` to fetch authenticated user profile information.
|
|
56
|
+
- `revokeToken()` to revoke issued tokens.
|
|
57
|
+
- In-memory **token caching** with early refresh via `TokenCache`.
|
|
58
|
+
- TypeScript support with full type definitions (`types.ts`).
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
- N/A (initial release).
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
- N/A (initial release).
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## [Unreleased]
|
|
69
|
+
### Planned
|
|
70
|
+
- Add PKCE (Proof Key for Code Exchange) support for better security.
|
|
71
|
+
- Persistent caching options (file system or Redis).
|
|
72
|
+
- Improved browser compatibility and npm publishing.
|
|
73
|
+
- Unit tests and CI/CD integration.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
**Repository:** [kerliix-corp/kerliix-oauth-nodejs](https://github.com/kerliix-corp/kerliix-oauth-nodejs)
|
|
78
|
+
**License:** MIT © Kerliix Corporation
|
package/dist/cache.d.ts
ADDED
package/dist/cache.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class TokenCache {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.tokenData = null;
|
|
4
|
+
}
|
|
5
|
+
set(token) {
|
|
6
|
+
token.created_at = Math.floor(Date.now() / 1000);
|
|
7
|
+
this.tokenData = token;
|
|
8
|
+
}
|
|
9
|
+
get() {
|
|
10
|
+
if (!this.tokenData)
|
|
11
|
+
return null;
|
|
12
|
+
const now = Math.floor(Date.now() / 1000);
|
|
13
|
+
const expiry = (this.tokenData.created_at || 0) + (this.tokenData.expires_in || 0);
|
|
14
|
+
if (now >= expiry - 30) {
|
|
15
|
+
console.warn("TokenCache: token expired or near expiry");
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return this.tokenData;
|
|
19
|
+
}
|
|
20
|
+
clear() {
|
|
21
|
+
this.tokenData = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { KerliixConfig, TokenResponse, UserInfo } from "./types.js";
|
|
2
|
+
export default class KerliixOAuth {
|
|
3
|
+
private config;
|
|
4
|
+
private cache;
|
|
5
|
+
constructor(config: KerliixConfig);
|
|
6
|
+
getAuthUrl(scopes?: string[], state?: string): string;
|
|
7
|
+
exchangeCodeForToken(code: string): Promise<TokenResponse>;
|
|
8
|
+
refreshTokenIfNeeded(): Promise<TokenResponse | null>;
|
|
9
|
+
getUserInfo(accessToken?: string): Promise<UserInfo>;
|
|
10
|
+
revokeToken(token: string): Promise<boolean>;
|
|
11
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { TokenCache } from "./cache.js";
|
|
2
|
+
export default class KerliixOAuth {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
if (!config.clientId || !config.redirectUri || !config.baseUrl)
|
|
5
|
+
throw new Error("Missing required config: clientId, redirectUri, or baseUrl");
|
|
6
|
+
this.config = {
|
|
7
|
+
...config,
|
|
8
|
+
baseUrl: config.baseUrl.replace(/\/$/, "")
|
|
9
|
+
};
|
|
10
|
+
this.cache = new TokenCache();
|
|
11
|
+
}
|
|
12
|
+
getAuthUrl(scopes = ["openid", "profile", "email"], state = "") {
|
|
13
|
+
const params = new URLSearchParams({
|
|
14
|
+
client_id: this.config.clientId,
|
|
15
|
+
redirect_uri: this.config.redirectUri,
|
|
16
|
+
response_type: "code",
|
|
17
|
+
scope: scopes.join(" "),
|
|
18
|
+
state
|
|
19
|
+
});
|
|
20
|
+
return `${this.config.baseUrl}/oauth/authorize?${params.toString()}`;
|
|
21
|
+
}
|
|
22
|
+
async exchangeCodeForToken(code) {
|
|
23
|
+
if (!code)
|
|
24
|
+
throw new Error("exchangeCodeForToken: code is required");
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(`${this.config.baseUrl}/oauth/token`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
grant_type: "authorization_code",
|
|
31
|
+
code,
|
|
32
|
+
redirect_uri: this.config.redirectUri,
|
|
33
|
+
client_id: this.config.clientId,
|
|
34
|
+
client_secret: this.config.clientSecret
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
throw new Error(`Token exchange failed: ${res.statusText}`);
|
|
39
|
+
}
|
|
40
|
+
const token = await res.json();
|
|
41
|
+
this.cache.set(token);
|
|
42
|
+
return token;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error("exchangeCodeForToken error:", err);
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async refreshTokenIfNeeded() {
|
|
50
|
+
try {
|
|
51
|
+
const cached = this.cache.get();
|
|
52
|
+
if (cached)
|
|
53
|
+
return cached;
|
|
54
|
+
const last = this.cache["tokenData"];
|
|
55
|
+
if (!last?.refresh_token)
|
|
56
|
+
return null;
|
|
57
|
+
const res = await fetch(`${this.config.baseUrl}/oauth/token`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
grant_type: "refresh_token",
|
|
62
|
+
refresh_token: last.refresh_token,
|
|
63
|
+
client_id: this.config.clientId,
|
|
64
|
+
client_secret: this.config.clientSecret
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok)
|
|
68
|
+
throw new Error("Failed to refresh token");
|
|
69
|
+
const token = await res.json();
|
|
70
|
+
this.cache.set(token);
|
|
71
|
+
return token;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.error("refreshTokenIfNeeded error:", err);
|
|
75
|
+
return null; // Fail gracefully, user may need re-auth
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async getUserInfo(accessToken) {
|
|
79
|
+
try {
|
|
80
|
+
const token = accessToken || (await this.refreshTokenIfNeeded())?.access_token;
|
|
81
|
+
if (!token)
|
|
82
|
+
throw new Error("Missing access token");
|
|
83
|
+
const res = await fetch(`${this.config.baseUrl}/oauth/userinfo`, {
|
|
84
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok)
|
|
87
|
+
throw new Error("Failed to fetch user info");
|
|
88
|
+
return res.json();
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.error("getUserInfo error:", err);
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async revokeToken(token) {
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(`${this.config.baseUrl}/oauth/revoke`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
body: JSON.stringify({ token })
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
console.warn("revokeToken failed: server rejected token");
|
|
104
|
+
this.cache.clear(); // clear cache anyway
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
this.cache.clear();
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
console.error("revokeToken error:", err);
|
|
112
|
+
this.cache.clear();
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface KerliixConfig {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret?: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TokenResponse {
|
|
8
|
+
access_token: string;
|
|
9
|
+
refresh_token?: string;
|
|
10
|
+
token_type: string;
|
|
11
|
+
expires_in: number;
|
|
12
|
+
scope?: string;
|
|
13
|
+
created_at?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface UserInfo {
|
|
16
|
+
id: string;
|
|
17
|
+
email?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
picture?: string;
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kerliix-oauth",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Official Kerliix OAuth SDK for Node.js and browser",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"test": "node tests/client.test.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"oauth2",
|
|
15
|
+
"kerliix",
|
|
16
|
+
"nodejs",
|
|
17
|
+
"sdk",
|
|
18
|
+
"authentication",
|
|
19
|
+
"typescript",
|
|
20
|
+
"oauth"
|
|
21
|
+
],
|
|
22
|
+
"author": "Kerliix Corporation",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/kerliix-corp/kerliix-oauth-nodejs.git"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.6.3"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TokenResponse } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export class TokenCache {
|
|
4
|
+
private tokenData: TokenResponse | null = null;
|
|
5
|
+
|
|
6
|
+
set(token: TokenResponse) {
|
|
7
|
+
token.created_at = Math.floor(Date.now() / 1000);
|
|
8
|
+
this.tokenData = token;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get(): TokenResponse | null {
|
|
12
|
+
if (!this.tokenData) return null;
|
|
13
|
+
|
|
14
|
+
const now = Math.floor(Date.now() / 1000);
|
|
15
|
+
const expiry = (this.tokenData.created_at || 0) + (this.tokenData.expires_in || 0);
|
|
16
|
+
|
|
17
|
+
if (now >= expiry - 30) {
|
|
18
|
+
console.warn("TokenCache: token expired or near expiry");
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return this.tokenData;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
clear() {
|
|
26
|
+
this.tokenData = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { KerliixConfig, TokenResponse, UserInfo } from "./types.js";
|
|
2
|
+
import { TokenCache } from "./cache.js";
|
|
3
|
+
|
|
4
|
+
export default class KerliixOAuth {
|
|
5
|
+
private config: KerliixConfig;
|
|
6
|
+
private cache: TokenCache;
|
|
7
|
+
|
|
8
|
+
constructor(config: KerliixConfig) {
|
|
9
|
+
if (!config.clientId || !config.redirectUri || !config.baseUrl)
|
|
10
|
+
throw new Error("Missing required config: clientId, redirectUri, or baseUrl");
|
|
11
|
+
|
|
12
|
+
this.config = {
|
|
13
|
+
...config,
|
|
14
|
+
baseUrl: config.baseUrl.replace(/\/$/, "")
|
|
15
|
+
};
|
|
16
|
+
this.cache = new TokenCache();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getAuthUrl(scopes: string[] = ["openid", "profile", "email"], state = ""): string {
|
|
20
|
+
const params = new URLSearchParams({
|
|
21
|
+
client_id: this.config.clientId,
|
|
22
|
+
redirect_uri: this.config.redirectUri,
|
|
23
|
+
response_type: "code",
|
|
24
|
+
scope: scopes.join(" "),
|
|
25
|
+
state
|
|
26
|
+
});
|
|
27
|
+
return `${this.config.baseUrl}/oauth/authorize?${params.toString()}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async exchangeCodeForToken(code: string): Promise<TokenResponse> {
|
|
31
|
+
if (!code) throw new Error("exchangeCodeForToken: code is required");
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`${this.config.baseUrl}/oauth/token`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
grant_type: "authorization_code",
|
|
39
|
+
code,
|
|
40
|
+
redirect_uri: this.config.redirectUri,
|
|
41
|
+
client_id: this.config.clientId,
|
|
42
|
+
client_secret: this.config.clientSecret
|
|
43
|
+
})
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
throw new Error(`Token exchange failed: ${res.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const token = await res.json() as TokenResponse;
|
|
51
|
+
this.cache.set(token);
|
|
52
|
+
return token;
|
|
53
|
+
|
|
54
|
+
} catch (err: any) {
|
|
55
|
+
console.error("exchangeCodeForToken error:", err);
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async refreshTokenIfNeeded(): Promise<TokenResponse | null> {
|
|
61
|
+
try {
|
|
62
|
+
const cached = this.cache.get();
|
|
63
|
+
if (cached) return cached;
|
|
64
|
+
|
|
65
|
+
const last = this.cache["tokenData"];
|
|
66
|
+
if (!last?.refresh_token) return null;
|
|
67
|
+
|
|
68
|
+
const res = await fetch(`${this.config.baseUrl}/oauth/token`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
grant_type: "refresh_token",
|
|
73
|
+
refresh_token: last.refresh_token,
|
|
74
|
+
client_id: this.config.clientId,
|
|
75
|
+
client_secret: this.config.clientSecret
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!res.ok) throw new Error("Failed to refresh token");
|
|
80
|
+
|
|
81
|
+
const token = await res.json() as TokenResponse;
|
|
82
|
+
this.cache.set(token);
|
|
83
|
+
return token;
|
|
84
|
+
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
console.error("refreshTokenIfNeeded error:", err);
|
|
87
|
+
return null; // Fail gracefully, user may need re-auth
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getUserInfo(accessToken?: string): Promise<UserInfo> {
|
|
92
|
+
try {
|
|
93
|
+
const token = accessToken || (await this.refreshTokenIfNeeded())?.access_token;
|
|
94
|
+
if (!token) throw new Error("Missing access token");
|
|
95
|
+
|
|
96
|
+
const res = await fetch(`${this.config.baseUrl}/oauth/userinfo`, {
|
|
97
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!res.ok) throw new Error("Failed to fetch user info");
|
|
101
|
+
return res.json() as Promise<UserInfo>;
|
|
102
|
+
|
|
103
|
+
} catch (err: any) {
|
|
104
|
+
console.error("getUserInfo error:", err);
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async revokeToken(token: string): Promise<boolean> {
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(`${this.config.baseUrl}/oauth/revoke`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "Content-Type": "application/json" },
|
|
114
|
+
body: JSON.stringify({ token })
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
console.warn("revokeToken failed: server rejected token");
|
|
119
|
+
this.cache.clear(); // clear cache anyway
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.cache.clear();
|
|
124
|
+
return true;
|
|
125
|
+
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
console.error("revokeToken error:", err);
|
|
128
|
+
this.cache.clear();
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface KerliixConfig {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret?: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TokenResponse {
|
|
9
|
+
access_token: string;
|
|
10
|
+
refresh_token?: string;
|
|
11
|
+
token_type: string;
|
|
12
|
+
expires_in: number;
|
|
13
|
+
scope?: string;
|
|
14
|
+
created_at?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UserInfo {
|
|
18
|
+
id: string;
|
|
19
|
+
email?: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
picture?: string;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import KerliixOAuth from "../dist/index.js";
|
|
2
|
+
|
|
3
|
+
(async () => {
|
|
4
|
+
const client = new KerliixOAuth({
|
|
5
|
+
clientId: "demo-client",
|
|
6
|
+
redirectUri: "https://example.com/callback",
|
|
7
|
+
baseUrl: "https://api.kerliix.com"
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
console.log("✅ Authorization URL:");
|
|
11
|
+
console.log(client.getAuthUrl(["openid", "profile", "email"], "test123"));
|
|
12
|
+
})();
|
package/tsconfig.json
ADDED