yapid-js 1.0.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 +202 -0
- package/package.json +51 -0
- package/src/index.js +199 -0
- package/src/react.js +258 -0
- package/src/server.js +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# yapid-js
|
|
2
|
+
|
|
3
|
+
Official JavaScript SDK for [YapID](https://id.yaphub.xyz) — Anonymous persistent identity for the web.
|
|
4
|
+
|
|
5
|
+
No email. No password. No tracking. 12 words. That's it.
|
|
6
|
+
|
|
7
|
+
[](https://npmjs.com/package/yapid-js)
|
|
8
|
+
[](https://id.yaphub.xyz)
|
|
9
|
+
[](https://github.com/yaphub/yapid/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install yapid-js
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Frontend (Browser)
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<!-- Option 1: Script Tag (no npm needed) -->
|
|
27
|
+
<script src="https://id.yaphub.xyz/yapid-button.js"></script>
|
|
28
|
+
<yapid-button onlogin="onLogin"></yapid-button>
|
|
29
|
+
|
|
30
|
+
<script>
|
|
31
|
+
function onLogin(session) {
|
|
32
|
+
console.log('Logged in:', session.accountId);
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// Option 2: npm
|
|
39
|
+
import { YapID } from 'yapid-js';
|
|
40
|
+
|
|
41
|
+
const client = new YapID();
|
|
42
|
+
|
|
43
|
+
// Redirect to YapID login
|
|
44
|
+
client.login('https://your-site.com/callback');
|
|
45
|
+
|
|
46
|
+
// After redirect back — get token from URL
|
|
47
|
+
const { token, state } = client.getTokenFromUrl();
|
|
48
|
+
if (client.verifyState(state)) {
|
|
49
|
+
const session = await client.verify(token);
|
|
50
|
+
console.log(session.accountId);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Backend (Node.js)
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import { verifyToken, yapidMiddleware } from 'yapid-js/server';
|
|
58
|
+
|
|
59
|
+
// Single verify
|
|
60
|
+
const session = await verifyToken(req.headers.authorization?.replace('Bearer ', ''));
|
|
61
|
+
if (session.valid) {
|
|
62
|
+
console.log(session.accountId, session.isPremium);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Express middleware
|
|
66
|
+
import express from 'express';
|
|
67
|
+
const app = express();
|
|
68
|
+
|
|
69
|
+
app.use('/api', yapidMiddleware({ required: true }));
|
|
70
|
+
|
|
71
|
+
app.get('/api/profile', (req, res) => {
|
|
72
|
+
res.json({ user: req.yapid });
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### React
|
|
77
|
+
|
|
78
|
+
```jsx
|
|
79
|
+
import { useYapID, YapIDProvider, YapIDButton } from 'yapid-js/react';
|
|
80
|
+
|
|
81
|
+
// Wrap your app
|
|
82
|
+
function App() {
|
|
83
|
+
return (
|
|
84
|
+
<YapIDProvider>
|
|
85
|
+
<MyApp />
|
|
86
|
+
</YapIDProvider>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Use the hook
|
|
91
|
+
function Profile() {
|
|
92
|
+
const { isLoggedIn, accountId, isPremium, login, logout } = useYapID();
|
|
93
|
+
|
|
94
|
+
if (!isLoggedIn) {
|
|
95
|
+
return <button onClick={login}>Sign in with YapID</button>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div>
|
|
100
|
+
<p>Welcome, {accountId}</p>
|
|
101
|
+
{isPremium && <p>★ Premium</p>}
|
|
102
|
+
<button onClick={logout}>Logout</button>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Or just use the pre-built button
|
|
108
|
+
function Header() {
|
|
109
|
+
return (
|
|
110
|
+
<YapIDButton
|
|
111
|
+
theme="dark"
|
|
112
|
+
size="medium"
|
|
113
|
+
onLogin={(session) => console.log(session)}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## API Reference
|
|
122
|
+
|
|
123
|
+
### Client (`yapid-js`)
|
|
124
|
+
|
|
125
|
+
| Method | Description |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `verify(token)` | Verify an access token |
|
|
128
|
+
| `login(redirectUrl)` | Redirect to YapID login |
|
|
129
|
+
| `getLoginUrl(redirectUrl)` | Get login URL without redirecting |
|
|
130
|
+
| `getTokenFromUrl()` | Extract token from URL hash after redirect |
|
|
131
|
+
| `verifyState(state)` | Verify CSRF state |
|
|
132
|
+
| `getUserInfo(token)` | Get user info (OIDC) |
|
|
133
|
+
| `refresh(refreshToken)` | Refresh access token |
|
|
134
|
+
| `logout(token)` | Logout current session |
|
|
135
|
+
| `logoutAll(token)` | Logout from all devices |
|
|
136
|
+
| `status()` | Check service status |
|
|
137
|
+
|
|
138
|
+
### Server (`yapid-js/server`)
|
|
139
|
+
|
|
140
|
+
| Export | Description |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `verifyToken(token)` | Verify token server-side |
|
|
143
|
+
| `yapidMiddleware(options)` | Express.js middleware |
|
|
144
|
+
| `YapIDServer` | Server SDK class |
|
|
145
|
+
|
|
146
|
+
### React (`yapid-js/react`)
|
|
147
|
+
|
|
148
|
+
| Export | Description |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `useYapID()` | React Hook |
|
|
151
|
+
| `YapIDProvider` | Context Provider |
|
|
152
|
+
| `YapIDButton` | Pre-built React Button |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Session Object
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
{
|
|
160
|
+
valid: boolean,
|
|
161
|
+
sub: string, // accountId (UUID)
|
|
162
|
+
accountId: string, // alias for sub
|
|
163
|
+
yapid_premium: boolean,
|
|
164
|
+
isPremium: boolean, // alias
|
|
165
|
+
yapid_avatar: string,
|
|
166
|
+
yapid_name: string | null,
|
|
167
|
+
scope: string, // 'openid profile'
|
|
168
|
+
expires_in: number, // seconds
|
|
169
|
+
profile: {
|
|
170
|
+
displayName: string | null,
|
|
171
|
+
avatarSeed: string,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Privacy
|
|
179
|
+
|
|
180
|
+
YapID has a **93% privacy score** — higher than any commercial alternative:
|
|
181
|
+
|
|
182
|
+
| Service | Privacy Score |
|
|
183
|
+
|---|---|
|
|
184
|
+
| **YapID** | **93%** |
|
|
185
|
+
| Wallet Connect | 67% |
|
|
186
|
+
| Auth0 / Clerk | 41% |
|
|
187
|
+
| Sign in with Google | 12% |
|
|
188
|
+
|
|
189
|
+
- ✓ No email required
|
|
190
|
+
- ✓ No IP address stored
|
|
191
|
+
- ✓ AES-256-GCM session encryption
|
|
192
|
+
- ✓ Ed25519 cryptographic signatures
|
|
193
|
+
- ✓ SHA-256 wallet hash (server never sees public key)
|
|
194
|
+
- ✓ No third-party tracking
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
BSL 1.1 — Source available, commercial competing use restricted for 4 years. Converts to Apache 2.0 in 2030.
|
|
201
|
+
|
|
202
|
+
[Full License](https://github.com/yaphub/yapid/blob/main/LICENSE)
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yapid-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official JavaScript SDK for YapID — Anonymous persistent identity for the web",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"module": "./src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js",
|
|
10
|
+
"./server": "./src/server.js",
|
|
11
|
+
"./react": "./src/react.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src/",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"yapid",
|
|
20
|
+
"yap",
|
|
21
|
+
"anonymous",
|
|
22
|
+
"identity",
|
|
23
|
+
"auth",
|
|
24
|
+
"oauth",
|
|
25
|
+
"privacy",
|
|
26
|
+
"web3",
|
|
27
|
+
"bip39",
|
|
28
|
+
"ed25519"
|
|
29
|
+
],
|
|
30
|
+
"author": "YAP <contact@cabalspy.xyz>",
|
|
31
|
+
"license": "BSL-1.1",
|
|
32
|
+
"homepage": "https://id.yaphub.xyz",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/yaphub/yapid-js"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/yaphub/yapid-js/issues"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=17.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"react": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* yapid-js
|
|
3
|
+
* =========
|
|
4
|
+
* Official JavaScript/TypeScript SDK for YapID
|
|
5
|
+
* Anonymous persistent identity for the web.
|
|
6
|
+
*
|
|
7
|
+
* npm install yapid-js
|
|
8
|
+
*
|
|
9
|
+
* Usage (Browser):
|
|
10
|
+
* import { YapID } from 'yapid-js';
|
|
11
|
+
* const yapid = new YapID();
|
|
12
|
+
* const session = await yapid.verify(token);
|
|
13
|
+
*
|
|
14
|
+
* Usage (Node.js / Backend):
|
|
15
|
+
* import { verifyToken } from 'yapid-js/server';
|
|
16
|
+
* const result = await verifyToken(token);
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const YAPID_ENDPOINT = 'https://id.yaphub.xyz';
|
|
20
|
+
|
|
21
|
+
// ── Types ─────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} YapIDSession
|
|
24
|
+
* @property {boolean} valid
|
|
25
|
+
* @property {string} sub - accountId (UUID)
|
|
26
|
+
* @property {string} accountId - Alias für sub (Rückwärtskompatibel)
|
|
27
|
+
* @property {boolean} yapid_premium
|
|
28
|
+
* @property {boolean} isPremium - Alias für yapid_premium
|
|
29
|
+
* @property {string} yapid_avatar
|
|
30
|
+
* @property {string|null} yapid_name
|
|
31
|
+
* @property {string} scope
|
|
32
|
+
* @property {number} expires_in
|
|
33
|
+
* @property {Object} profile
|
|
34
|
+
* @property {string|null} profile.displayName
|
|
35
|
+
* @property {string} profile.avatarSeed
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// ── Client SDK ────────────────────────────────────────────────
|
|
39
|
+
export class YapID {
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
this.endpoint = options.endpoint || YAPID_ENDPOINT;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Token verifizieren
|
|
46
|
+
* @param {string} token - Access Token
|
|
47
|
+
* @returns {Promise<YapIDSession>}
|
|
48
|
+
*/
|
|
49
|
+
async verify(token) {
|
|
50
|
+
const res = await fetch(`${this.endpoint}/api/verify`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ access_token: token, token }),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) throw new Error(`YapID verify failed: ${res.status}`);
|
|
56
|
+
return res.json();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Login-URL generieren (OAuth Redirect)
|
|
61
|
+
* @param {string} redirectUrl - URL zu der nach Login zurückgeleitet wird
|
|
62
|
+
* @param {Object} options
|
|
63
|
+
* @param {string} options.scope - Standard: 'openid profile'
|
|
64
|
+
* @param {string} options.state - CSRF State (optional, wird auto-generiert)
|
|
65
|
+
* @returns {{ url: string, state: string }}
|
|
66
|
+
*/
|
|
67
|
+
getLoginUrl(redirectUrl, options = {}) {
|
|
68
|
+
const state = options.state || this._generateState();
|
|
69
|
+
const url = new URL(`${this.endpoint}/login`);
|
|
70
|
+
url.searchParams.set('redirect', redirectUrl);
|
|
71
|
+
url.searchParams.set('state', state);
|
|
72
|
+
url.searchParams.set('scope', options.scope || 'openid profile');
|
|
73
|
+
return { url: url.toString(), state };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Redirect zu YapID Login
|
|
78
|
+
* @param {string} redirectUrl
|
|
79
|
+
* @param {Object} options
|
|
80
|
+
*/
|
|
81
|
+
login(redirectUrl, options = {}) {
|
|
82
|
+
if (typeof window === 'undefined') throw new Error('login() only works in browser');
|
|
83
|
+
const { url, state } = this.getLoginUrl(redirectUrl || window.location.href, options);
|
|
84
|
+
sessionStorage.setItem('yapid_state', state);
|
|
85
|
+
window.location.href = url;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Token aus URL Hash nach Redirect extrahieren
|
|
90
|
+
* @returns {{ token: string|null, refreshToken: string|null, state: string|null }}
|
|
91
|
+
*/
|
|
92
|
+
getTokenFromUrl() {
|
|
93
|
+
if (typeof window === 'undefined') return { token: null, refreshToken: null, state: null };
|
|
94
|
+
const hash = window.location.hash;
|
|
95
|
+
if (!hash.includes('yapid_token=')) return { token: null, refreshToken: null, state: null };
|
|
96
|
+
const params = new URLSearchParams(hash.slice(1));
|
|
97
|
+
return {
|
|
98
|
+
token: params.get('yapid_token'),
|
|
99
|
+
refreshToken: params.get('yapid_refresh'),
|
|
100
|
+
state: params.get('state'),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* State verifizieren (CSRF Schutz)
|
|
106
|
+
* @param {string} returnedState
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
verifyState(returnedState) {
|
|
110
|
+
if (typeof window === 'undefined') return true;
|
|
111
|
+
const saved = sessionStorage.getItem('yapid_state');
|
|
112
|
+
sessionStorage.removeItem('yapid_state');
|
|
113
|
+
if (!saved || !returnedState) return true; // kein State = optional
|
|
114
|
+
return saved === returnedState;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* UserInfo abrufen
|
|
119
|
+
* @param {string} token
|
|
120
|
+
* @returns {Promise<Object>}
|
|
121
|
+
*/
|
|
122
|
+
async getUserInfo(token) {
|
|
123
|
+
const res = await fetch(`${this.endpoint}/api/userinfo`, {
|
|
124
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
125
|
+
});
|
|
126
|
+
if (!res.ok) throw new Error(`YapID userinfo failed: ${res.status}`);
|
|
127
|
+
return res.json();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Token refreshen
|
|
132
|
+
* @param {string} refreshToken
|
|
133
|
+
* @returns {Promise<Object>}
|
|
134
|
+
*/
|
|
135
|
+
async refresh(refreshToken) {
|
|
136
|
+
const res = await fetch(`${this.endpoint}/auth/refresh`, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
140
|
+
});
|
|
141
|
+
if (!res.ok) throw new Error(`YapID refresh failed: ${res.status}`);
|
|
142
|
+
return res.json();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Logout (diese Session)
|
|
147
|
+
* @param {string} token
|
|
148
|
+
*/
|
|
149
|
+
async logout(token) {
|
|
150
|
+
await fetch(`${this.endpoint}/auth/logout`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { 'Content-Type': 'application/json' },
|
|
153
|
+
body: JSON.stringify({ access_token: token }),
|
|
154
|
+
}).catch(() => {});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Logout von allen Geräten
|
|
159
|
+
* @param {string} token
|
|
160
|
+
*/
|
|
161
|
+
async logoutAll(token) {
|
|
162
|
+
const res = await fetch(`${this.endpoint}/auth/logout-all`, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
body: JSON.stringify({ access_token: token }),
|
|
166
|
+
});
|
|
167
|
+
if (!res.ok) throw new Error(`YapID logout-all failed: ${res.status}`);
|
|
168
|
+
return res.json();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Service Status prüfen
|
|
173
|
+
* @returns {Promise<Object>}
|
|
174
|
+
*/
|
|
175
|
+
async status() {
|
|
176
|
+
const res = await fetch(`${this.endpoint}/api/status`);
|
|
177
|
+
return res.json();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_generateState() {
|
|
181
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
182
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(16)))
|
|
183
|
+
.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
184
|
+
}
|
|
185
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Convenience Exports ───────────────────────────────────────
|
|
190
|
+
export const yapid = new YapID();
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Quick verify — für einfache Nutzung
|
|
194
|
+
* @param {string} token
|
|
195
|
+
* @returns {Promise<YapIDSession>}
|
|
196
|
+
*/
|
|
197
|
+
export const verifyToken = (token) => yapid.verify(token);
|
|
198
|
+
|
|
199
|
+
export default YapID;
|
package/src/react.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* yapid-js/react
|
|
3
|
+
* ===============
|
|
4
|
+
* React Hook und Komponenten für YapID
|
|
5
|
+
*
|
|
6
|
+
* import { useYapID, YapIDProvider, YapIDButton } from 'yapid-js/react';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useEffect, useCallback, createContext, useContext } from 'react';
|
|
10
|
+
import { YapID } from './index.js';
|
|
11
|
+
|
|
12
|
+
const YAPID_ENDPOINT = 'https://id.yaphub.xyz';
|
|
13
|
+
|
|
14
|
+
// ── Context ───────────────────────────────────────────────────
|
|
15
|
+
const YapIDContext = createContext(null);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* YapID Provider — wraps your app
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* function App() {
|
|
22
|
+
* return (
|
|
23
|
+
* <YapIDProvider>
|
|
24
|
+
* <YourApp />
|
|
25
|
+
* </YapIDProvider>
|
|
26
|
+
* );
|
|
27
|
+
* }
|
|
28
|
+
*/
|
|
29
|
+
export function YapIDProvider({ children, options = {} }) {
|
|
30
|
+
const value = useYapIDInternal(options);
|
|
31
|
+
return React.createElement(YapIDContext.Provider, { value }, children);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Internal Hook ─────────────────────────────────────────────
|
|
35
|
+
function useYapIDInternal(options = {}) {
|
|
36
|
+
const [session, setSession] = useState(null);
|
|
37
|
+
const [loading, setLoading] = useState(true);
|
|
38
|
+
const [error, setError] = useState(null);
|
|
39
|
+
|
|
40
|
+
const client = new YapID(options);
|
|
41
|
+
|
|
42
|
+
// Redirect-Return prüfen und Token verifizieren
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
async function init() {
|
|
45
|
+
try {
|
|
46
|
+
const { token, refreshToken, state } = client.getTokenFromUrl();
|
|
47
|
+
|
|
48
|
+
if (token) {
|
|
49
|
+
// State verifizieren
|
|
50
|
+
if (!client.verifyState(state)) {
|
|
51
|
+
setError('State mismatch — possible CSRF attack');
|
|
52
|
+
setLoading(false);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// URL Hash entfernen
|
|
57
|
+
window.history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
58
|
+
|
|
59
|
+
const data = await client.verify(token);
|
|
60
|
+
if (data.valid) {
|
|
61
|
+
setSession({ ...data, token, refreshToken });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
setError(e.message);
|
|
66
|
+
} finally {
|
|
67
|
+
setLoading(false);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
init();
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Login — redirectet zu YapID
|
|
76
|
+
* @param {string} redirectUrl - Standard: aktuelle URL
|
|
77
|
+
*/
|
|
78
|
+
const login = useCallback((redirectUrl) => {
|
|
79
|
+
client.login(redirectUrl || window.location.href);
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Token verifizieren
|
|
84
|
+
* @param {string} token
|
|
85
|
+
*/
|
|
86
|
+
const verify = useCallback(async (token) => {
|
|
87
|
+
try {
|
|
88
|
+
const data = await client.verify(token);
|
|
89
|
+
if (data.valid) setSession({ ...data, token });
|
|
90
|
+
return data;
|
|
91
|
+
} catch (e) {
|
|
92
|
+
setError(e.message);
|
|
93
|
+
return { valid: false };
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Logout
|
|
99
|
+
*/
|
|
100
|
+
const logout = useCallback(async () => {
|
|
101
|
+
if (session?.token) {
|
|
102
|
+
await client.logout(session.token);
|
|
103
|
+
}
|
|
104
|
+
setSession(null);
|
|
105
|
+
}, [session]);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Logout von allen Geräten
|
|
109
|
+
*/
|
|
110
|
+
const logoutAll = useCallback(async () => {
|
|
111
|
+
if (session?.token) {
|
|
112
|
+
await client.logoutAll(session.token);
|
|
113
|
+
}
|
|
114
|
+
setSession(null);
|
|
115
|
+
}, [session]);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
session,
|
|
119
|
+
loading,
|
|
120
|
+
error,
|
|
121
|
+
isLoggedIn: !!session?.valid,
|
|
122
|
+
accountId: session?.sub || session?.accountId || null,
|
|
123
|
+
isPremium: session?.yapid_premium || session?.isPremium || false,
|
|
124
|
+
profile: session?.profile || null,
|
|
125
|
+
token: session?.token || null,
|
|
126
|
+
login,
|
|
127
|
+
verify,
|
|
128
|
+
logout,
|
|
129
|
+
logoutAll,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* useYapID Hook
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* function MyComponent() {
|
|
138
|
+
* const { isLoggedIn, accountId, isPremium, login, logout } = useYapID();
|
|
139
|
+
*
|
|
140
|
+
* if (!isLoggedIn) {
|
|
141
|
+
* return <button onClick={login}>Sign in with YapID</button>;
|
|
142
|
+
* }
|
|
143
|
+
*
|
|
144
|
+
* return (
|
|
145
|
+
* <div>
|
|
146
|
+
* <p>Welcome, {accountId}</p>
|
|
147
|
+
* {isPremium && <p>★ Premium</p>}
|
|
148
|
+
* <button onClick={logout}>Logout</button>
|
|
149
|
+
* </div>
|
|
150
|
+
* );
|
|
151
|
+
* }
|
|
152
|
+
*/
|
|
153
|
+
export function useYapID(options = {}) {
|
|
154
|
+
const context = useContext(YapIDContext);
|
|
155
|
+
// Wenn kein Provider — standalone Hook
|
|
156
|
+
if (!context) return useYapIDInternal(options);
|
|
157
|
+
return context;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* YapIDButton React Komponente
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* <YapIDButton
|
|
165
|
+
* theme="dark"
|
|
166
|
+
* size="medium"
|
|
167
|
+
* text="Sign in with YapID"
|
|
168
|
+
* onLogin={(session) => console.log(session)}
|
|
169
|
+
* />
|
|
170
|
+
*/
|
|
171
|
+
export function YapIDButton({ theme = 'dark', size = 'medium', text = 'Sign in with YapID', onLogin, className, style }) {
|
|
172
|
+
const { isLoggedIn, accountId, isPremium, profile, login, logout } = useYapID();
|
|
173
|
+
const [showPanel, setShowPanel] = useState(false);
|
|
174
|
+
|
|
175
|
+
const sizes = {
|
|
176
|
+
small: { padding: '6px 14px', fontSize: '11px', height: '32px' },
|
|
177
|
+
medium: { padding: '9px 20px', fontSize: '12px', height: '40px' },
|
|
178
|
+
large: { padding: '12px 28px', fontSize: '14px', height: '48px' },
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const themes = {
|
|
182
|
+
dark: { background: '#111', color: '#f0f0f0', border: '1px solid #2a2a2a' },
|
|
183
|
+
light: { background: '#fff', color: '#111', border: '1px solid #e0e0e0' },
|
|
184
|
+
purple: { background: '#7c3aed', color: '#fff', border: '1px solid #6d28d9' },
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const baseStyle = {
|
|
188
|
+
display: 'inline-flex',
|
|
189
|
+
alignItems: 'center',
|
|
190
|
+
gap: '8px',
|
|
191
|
+
fontFamily: "'Space Mono', monospace",
|
|
192
|
+
fontWeight: 700,
|
|
193
|
+
cursor: 'pointer',
|
|
194
|
+
letterSpacing: '.04em',
|
|
195
|
+
borderRadius: '8px',
|
|
196
|
+
transition: 'all .2s',
|
|
197
|
+
whiteSpace: 'nowrap',
|
|
198
|
+
...sizes[size] || sizes.medium,
|
|
199
|
+
...themes[theme] || themes.dark,
|
|
200
|
+
...style,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const handleClick = () => {
|
|
204
|
+
if (isLoggedIn) setShowPanel(p => !p);
|
|
205
|
+
else login();
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!isLoggedIn || !onLogin) return;
|
|
210
|
+
// onLogin nur nach Redirect-Return aufrufen — wird in useYapID gehandhabt
|
|
211
|
+
}, [isLoggedIn]);
|
|
212
|
+
|
|
213
|
+
const label = isLoggedIn
|
|
214
|
+
? (profile?.displayName || (accountId || '').slice(0, 8) + '…')
|
|
215
|
+
: text;
|
|
216
|
+
|
|
217
|
+
return React.createElement('div', { style: { position: 'relative', display: 'inline-block' } },
|
|
218
|
+
React.createElement('button', { onClick: handleClick, style: baseStyle, className },
|
|
219
|
+
React.createElement('img', {
|
|
220
|
+
src: theme === 'light'
|
|
221
|
+
? `${YAPID_ENDPOINT}/yaphub-dark.png`
|
|
222
|
+
: `${YAPID_ENDPOINT}/yaphub.png`,
|
|
223
|
+
alt: 'YapID',
|
|
224
|
+
style: { width: '14px', height: '14px', objectFit: 'contain' },
|
|
225
|
+
}),
|
|
226
|
+
label,
|
|
227
|
+
isLoggedIn && React.createElement('span', {
|
|
228
|
+
style: {
|
|
229
|
+
width: '7px', height: '7px', borderRadius: '50%',
|
|
230
|
+
background: '#22c55e', boxShadow: '0 0 6px #22c55e',
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
),
|
|
234
|
+
showPanel && isLoggedIn && React.createElement('div', {
|
|
235
|
+
style: {
|
|
236
|
+
position: 'absolute', top: 'calc(100% + 8px)', left: 0,
|
|
237
|
+
background: '#111', border: '1px solid #2a2a2a', borderRadius: '10px',
|
|
238
|
+
padding: '16px', zIndex: 9999, minWidth: '220px',
|
|
239
|
+
fontFamily: "'Space Mono', monospace", fontSize: '11px', color: '#888',
|
|
240
|
+
boxShadow: '0 8px 32px rgba(0,0,0,.7)',
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
React.createElement('div', { style: { color: '#a78bfa', fontWeight: 700, marginBottom: '6px', fontSize: '12px' } },
|
|
244
|
+
'YapID',
|
|
245
|
+
isPremium && React.createElement('span', {
|
|
246
|
+
style: { fontSize: '9px', color: '#f59e0b', border: '1px solid rgba(245,158,11,.3)', borderRadius: '8px', padding: '1px 6px', marginLeft: '6px' }
|
|
247
|
+
}, '★ Premium')
|
|
248
|
+
),
|
|
249
|
+
React.createElement('div', { style: { fontSize: '9px', color: '#444', wordBreak: 'break-all', marginBottom: '14px', lineHeight: 1.7 } }, accountId),
|
|
250
|
+
React.createElement('button', {
|
|
251
|
+
onClick: () => { logout(); setShowPanel(false); },
|
|
252
|
+
style: { width: '100%', padding: '7px', background: 'none', border: '1px solid #2a2a2a', borderRadius: '6px', color: '#666', fontFamily: 'inherit', fontSize: '10px', cursor: 'pointer' }
|
|
253
|
+
}, 'Logout')
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export default useYapID;
|
package/src/server.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* yapid-js/server
|
|
3
|
+
* ================
|
|
4
|
+
* Node.js Backend SDK für YapID
|
|
5
|
+
* Für serverseitige Token-Verifikation
|
|
6
|
+
*
|
|
7
|
+
* import { verifyToken, YapIDServer } from 'yapid-js/server';
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const YAPID_ENDPOINT = 'https://id.yaphub.xyz';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Token serverseitig verifizieren
|
|
14
|
+
* @param {string} token - Access Token aus dem Request
|
|
15
|
+
* @returns {Promise<import('./index.js').YapIDSession>}
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Express.js Middleware
|
|
19
|
+
* app.use(async (req, res, next) => {
|
|
20
|
+
* const token = req.headers.authorization?.replace('Bearer ', '');
|
|
21
|
+
* const session = await verifyToken(token);
|
|
22
|
+
* if (!session.valid) return res.status(401).json({ error: 'Unauthorized' });
|
|
23
|
+
* req.user = session;
|
|
24
|
+
* next();
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
export async function verifyToken(token, options = {}) {
|
|
28
|
+
if (!token) return { valid: false };
|
|
29
|
+
const endpoint = options.endpoint || YAPID_ENDPOINT;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(`${endpoint}/api/verify`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ access_token: token, token }),
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) return { valid: false };
|
|
38
|
+
return res.json();
|
|
39
|
+
} catch {
|
|
40
|
+
return { valid: false };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Express.js Middleware
|
|
46
|
+
* @param {Object} options
|
|
47
|
+
* @param {boolean} options.required - Wenn true, gibt 401 zurück wenn kein Token
|
|
48
|
+
* @returns {Function} Express Middleware
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* import { yapidMiddleware } from 'yapid-js/server';
|
|
52
|
+
* app.use(yapidMiddleware());
|
|
53
|
+
* app.get('/protected', (req, res) => {
|
|
54
|
+
* res.json({ user: req.yapid });
|
|
55
|
+
* });
|
|
56
|
+
*/
|
|
57
|
+
export function yapidMiddleware(options = {}) {
|
|
58
|
+
return async (req, res, next) => {
|
|
59
|
+
const token = req.headers.authorization?.replace('Bearer ', '')
|
|
60
|
+
|| req.query?.yapid_token
|
|
61
|
+
|| req.body?.yapid_token;
|
|
62
|
+
|
|
63
|
+
if (!token) {
|
|
64
|
+
if (options.required) {
|
|
65
|
+
return res.status(401).json({ error: 'YapID token required' });
|
|
66
|
+
}
|
|
67
|
+
req.yapid = null;
|
|
68
|
+
return next();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const session = await verifyToken(token, options);
|
|
72
|
+
if (!session.valid) {
|
|
73
|
+
if (options.required) {
|
|
74
|
+
return res.status(401).json({ error: 'Invalid YapID token' });
|
|
75
|
+
}
|
|
76
|
+
req.yapid = null;
|
|
77
|
+
return next();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
req.yapid = session;
|
|
81
|
+
next();
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Server SDK Klasse
|
|
87
|
+
*/
|
|
88
|
+
export class YapIDServer {
|
|
89
|
+
constructor(options = {}) {
|
|
90
|
+
this.endpoint = options.endpoint || YAPID_ENDPOINT;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async verify(token) { return verifyToken(token, { endpoint: this.endpoint }); }
|
|
94
|
+
middleware(options = {}) { return yapidMiddleware({ ...options, endpoint: this.endpoint }); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default YapIDServer;
|