@eudi-verify/server 0.1.0 → 0.1.2
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 +67 -58
- package/dist/index.d.ts +3 -3
- package/dist/index.js +14 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,11 +15,11 @@ import {
|
|
|
15
15
|
createVerifierHandlers,
|
|
16
16
|
OpenEudiEngine,
|
|
17
17
|
MemoryKVStore,
|
|
18
|
-
} from
|
|
18
|
+
} from "@eudi-verify/server";
|
|
19
19
|
|
|
20
20
|
// 1. Create engine and store
|
|
21
|
-
const BASE_URL = process.env.BASE_URL ||
|
|
22
|
-
const engine = new OpenEudiEngine({ mode:
|
|
21
|
+
const BASE_URL = process.env.BASE_URL || "http://localhost:3000/api/eudi";
|
|
22
|
+
const engine = new OpenEudiEngine({ mode: "demo", baseUrl: BASE_URL });
|
|
23
23
|
const store = new MemoryKVStore();
|
|
24
24
|
|
|
25
25
|
// 2. Create handlers
|
|
@@ -27,7 +27,7 @@ const handlers = createVerifierHandlers({
|
|
|
27
27
|
engine,
|
|
28
28
|
store,
|
|
29
29
|
baseUrl: BASE_URL,
|
|
30
|
-
mode:
|
|
30
|
+
mode: "demo",
|
|
31
31
|
tokenSecret: process.env.TOKEN_SECRET!, // 32+ chars
|
|
32
32
|
});
|
|
33
33
|
|
|
@@ -40,11 +40,11 @@ const handlers = createVerifierHandlers({
|
|
|
40
40
|
### Node.js HTTP
|
|
41
41
|
|
|
42
42
|
```ts
|
|
43
|
-
import http from
|
|
43
|
+
import http from "node:http";
|
|
44
44
|
|
|
45
45
|
function buildContext(req, params = {}, body = undefined) {
|
|
46
46
|
return {
|
|
47
|
-
ip: req.socket.remoteAddress ??
|
|
47
|
+
ip: req.socket.remoteAddress ?? "127.0.0.1",
|
|
48
48
|
origin: req.headers.origin,
|
|
49
49
|
params,
|
|
50
50
|
body,
|
|
@@ -53,11 +53,13 @@ function buildContext(req, params = {}, body = undefined) {
|
|
|
53
53
|
|
|
54
54
|
const server = http.createServer(async (req, res) => {
|
|
55
55
|
const url = new URL(req.url!, `http://${req.headers.host}`);
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
// Route to handlers
|
|
58
|
-
if (url.pathname ===
|
|
58
|
+
if (url.pathname === "/sessions" && req.method === "POST") {
|
|
59
59
|
const body = await readBody(req);
|
|
60
|
-
const result = await handlers.createSession(
|
|
60
|
+
const result = await handlers.createSession(
|
|
61
|
+
buildContext(req, {}, JSON.parse(body)),
|
|
62
|
+
);
|
|
61
63
|
sendJson(res, result.status, result.body, result.headers);
|
|
62
64
|
}
|
|
63
65
|
// ... other routes
|
|
@@ -67,36 +69,40 @@ const server = http.createServer(async (req, res) => {
|
|
|
67
69
|
### Express
|
|
68
70
|
|
|
69
71
|
```ts
|
|
70
|
-
import express from
|
|
72
|
+
import express from "express";
|
|
71
73
|
|
|
72
74
|
const app = express();
|
|
73
75
|
app.use(express.json());
|
|
74
76
|
|
|
75
77
|
function buildContext(req, params = {}, body = undefined) {
|
|
76
78
|
return {
|
|
77
|
-
ip: req.ip ??
|
|
79
|
+
ip: req.ip ?? "127.0.0.1",
|
|
78
80
|
origin: req.headers.origin,
|
|
79
81
|
params,
|
|
80
82
|
body,
|
|
81
83
|
};
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
app.post(
|
|
86
|
+
app.post("/sessions", async (req, res) => {
|
|
85
87
|
const result = await handlers.createSession(buildContext(req, {}, req.body));
|
|
86
88
|
res.status(result.status).set(result.headers).json(result.body);
|
|
87
89
|
});
|
|
88
90
|
|
|
89
|
-
app.get(
|
|
90
|
-
const result = await handlers.getSession(
|
|
91
|
+
app.get("/sessions/:id", async (req, res) => {
|
|
92
|
+
const result = await handlers.getSession(
|
|
93
|
+
buildContext(req, { sessionId: req.params.id }),
|
|
94
|
+
);
|
|
91
95
|
res.status(result.status).set(result.headers).json(result.body);
|
|
92
96
|
});
|
|
93
97
|
|
|
94
|
-
app.post(
|
|
95
|
-
const result = await handlers.cancelSession(
|
|
98
|
+
app.post("/sessions/:id/cancel", async (req, res) => {
|
|
99
|
+
const result = await handlers.cancelSession(
|
|
100
|
+
buildContext(req, { sessionId: req.params.id }),
|
|
101
|
+
);
|
|
96
102
|
res.status(result.status).set(result.headers).json(result.body);
|
|
97
103
|
});
|
|
98
104
|
|
|
99
|
-
app.post(
|
|
105
|
+
app.post("/tokens/verify", async (req, res) => {
|
|
100
106
|
const result = await handlers.verifyToken(buildContext(req, {}, req.body));
|
|
101
107
|
res.status(result.status).json(result.body);
|
|
102
108
|
});
|
|
@@ -105,21 +111,23 @@ app.post('/tokens/verify', async (req, res) => {
|
|
|
105
111
|
### Hono
|
|
106
112
|
|
|
107
113
|
```ts
|
|
108
|
-
import { Hono } from
|
|
114
|
+
import { Hono } from "hono";
|
|
109
115
|
|
|
110
116
|
const app = new Hono();
|
|
111
117
|
|
|
112
118
|
function buildContext(c, params = {}, body = undefined) {
|
|
113
119
|
return {
|
|
114
|
-
ip: c.req.header(
|
|
115
|
-
origin: c.req.header(
|
|
120
|
+
ip: c.req.header("x-forwarded-for") ?? "127.0.0.1",
|
|
121
|
+
origin: c.req.header("origin"),
|
|
116
122
|
params,
|
|
117
123
|
body,
|
|
118
124
|
};
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
app.post(
|
|
122
|
-
const result = await handlers.createSession(
|
|
127
|
+
app.post("/sessions", async (c) => {
|
|
128
|
+
const result = await handlers.createSession(
|
|
129
|
+
buildContext(c, {}, await c.req.json()),
|
|
130
|
+
);
|
|
123
131
|
return c.json(result.body, result.status, result.headers);
|
|
124
132
|
});
|
|
125
133
|
|
|
@@ -130,30 +138,30 @@ app.post('/sessions', async (c) => {
|
|
|
130
138
|
|
|
131
139
|
```ts
|
|
132
140
|
interface VerifierConfig {
|
|
133
|
-
engine: VerifierEngine;
|
|
134
|
-
store: IKVStore;
|
|
135
|
-
baseUrl: string;
|
|
136
|
-
mode:
|
|
137
|
-
tokenSecret: string;
|
|
138
|
-
tokenTtlMs?: number;
|
|
139
|
-
sessionTtlMs?: number;
|
|
141
|
+
engine: VerifierEngine; // OpenEudiEngine or MockEngine
|
|
142
|
+
store: IKVStore; // MemoryKVStore (or Redis for production)
|
|
143
|
+
baseUrl: string; // Public callback URL (e.g., https://example.com/api/eudi)
|
|
144
|
+
mode: "demo" | "production";
|
|
145
|
+
tokenSecret: string; // HMAC secret, 32+ characters
|
|
146
|
+
tokenTtlMs?: number; // Default: 300000 (5 min)
|
|
147
|
+
sessionTtlMs?: number; // Default: 300000 (5 min)
|
|
140
148
|
rateLimit?: {
|
|
141
|
-
maxRequests: number;
|
|
142
|
-
windowMs: number;
|
|
149
|
+
maxRequests: number; // Default: 10
|
|
150
|
+
windowMs: number; // Default: 60000 (1 min)
|
|
143
151
|
};
|
|
144
|
-
allowedOrigins?: string[];
|
|
152
|
+
allowedOrigins?: string[]; // CORS/Origin check (empty = allow all)
|
|
145
153
|
}
|
|
146
154
|
```
|
|
147
155
|
|
|
148
156
|
## Handlers
|
|
149
157
|
|
|
150
|
-
| Handler
|
|
151
|
-
|
|
152
|
-
| `createSession(body, ctx)` | `POST /sessions`
|
|
153
|
-
| `getSession(id)`
|
|
154
|
-
| `cancelSession(id)`
|
|
155
|
-
| `verifyToken(body)`
|
|
156
|
-
| `handleCallback(data)`
|
|
158
|
+
| Handler | Route | Description |
|
|
159
|
+
| -------------------------- | --------------------------- | --------------------------- |
|
|
160
|
+
| `createSession(body, ctx)` | `POST /sessions` | Create verification session |
|
|
161
|
+
| `getSession(id)` | `GET /sessions/:id` | Get session status |
|
|
162
|
+
| `cancelSession(id)` | `POST /sessions/:id/cancel` | Cancel active session |
|
|
163
|
+
| `verifyToken(body)` | `POST /tokens/verify` | Validate verification token |
|
|
164
|
+
| `handleCallback(data)` | `POST /callback` | Wallet callback (internal) |
|
|
157
165
|
|
|
158
166
|
## Error Boundaries
|
|
159
167
|
|
|
@@ -163,22 +171,22 @@ Handlers return `{ status, headers?, body }` — they **never throw**. Your fram
|
|
|
163
171
|
|
|
164
172
|
**1. HTTP errors** — returned as `{ error, message, details? }` with 4xx/5xx status:
|
|
165
173
|
|
|
166
|
-
| Status | `error` code
|
|
167
|
-
|
|
168
|
-
| 400
|
|
169
|
-
| 403
|
|
170
|
-
| 404
|
|
171
|
-
| 409
|
|
172
|
-
| 429
|
|
173
|
-
| 500
|
|
174
|
+
| Status | `error` code | Typical cause |
|
|
175
|
+
| ------ | ---------------- | ------------------------------ |
|
|
176
|
+
| 400 | `bad_request` | Invalid input |
|
|
177
|
+
| 403 | `forbidden` | Origin not in `allowedOrigins` |
|
|
178
|
+
| 404 | `not_found` | Session missing |
|
|
179
|
+
| 409 | `conflict` | Cancel on terminal session |
|
|
180
|
+
| 429 | `rate_limited` | Rate limit exceeded |
|
|
181
|
+
| 500 | `internal_error` | Engine failure on create |
|
|
174
182
|
|
|
175
183
|
**2. Session outcomes** — HTTP 200, check `body.status`:
|
|
176
184
|
|
|
177
|
-
| `status`
|
|
178
|
-
|
|
179
|
-
| `rejected` | User declined in wallet
|
|
180
|
-
| `expired`
|
|
181
|
-
| `error`
|
|
185
|
+
| `status` | Meaning |
|
|
186
|
+
| ---------- | ------------------------------- |
|
|
187
|
+
| `rejected` | User declined in wallet |
|
|
188
|
+
| `expired` | Session TTL elapsed |
|
|
189
|
+
| `error` | VP validation or engine failure |
|
|
182
190
|
|
|
183
191
|
These surface to your frontend via `GET /sessions/:id` polling, not via callback HTTP status.
|
|
184
192
|
|
|
@@ -198,12 +206,12 @@ To report callback-path failures server-side, inspect the session after handling
|
|
|
198
206
|
### Route adapter pattern
|
|
199
207
|
|
|
200
208
|
```ts
|
|
201
|
-
app.post(
|
|
209
|
+
app.post("/sessions", async (req, res) => {
|
|
202
210
|
const result = await handlers.createSession(buildContext(req, {}, req.body));
|
|
203
211
|
|
|
204
|
-
if (result.status >= 400 &&
|
|
212
|
+
if (result.status >= 400 && "error" in result.body) {
|
|
205
213
|
// Your error reporting hook
|
|
206
|
-
reportError({ handler:
|
|
214
|
+
reportError({ handler: "createSession", ...result.body });
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
res.status(result.status).set(result.headers).json(result.body);
|
|
@@ -225,11 +233,11 @@ After the widget emits a `verified` event with a token, validate it server-side:
|
|
|
225
233
|
|
|
226
234
|
```ts
|
|
227
235
|
// In your protected endpoint
|
|
228
|
-
app.post(
|
|
236
|
+
app.post("/checkout", async (req, res) => {
|
|
229
237
|
const { eudiToken } = req.body;
|
|
230
|
-
|
|
238
|
+
|
|
231
239
|
const result = await handlers.verifyToken({ token: eudiToken });
|
|
232
|
-
|
|
240
|
+
|
|
233
241
|
if (result.body.valid) {
|
|
234
242
|
// Token is valid, claims are verified
|
|
235
243
|
const { age_over_18, nationality } = result.body.claims;
|
|
@@ -246,6 +254,7 @@ app.post('/checkout', async (req, res) => {
|
|
|
246
254
|
⚠️ Demo mode accepts simulated credentials. **Never use in production.**
|
|
247
255
|
|
|
248
256
|
Demo mode is indicated by:
|
|
257
|
+
|
|
249
258
|
- Console warning on startup
|
|
250
259
|
- `X-Eudi-Mode: demo` header on all responses
|
|
251
260
|
|
package/dist/index.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ interface VerificationRequest {
|
|
|
39
39
|
* Flow: pending → waiting_for_wallet → verified|rejected|expired|error
|
|
40
40
|
* ↳ cancelled (from any non-terminal state)
|
|
41
41
|
*/
|
|
42
|
-
type SessionStatus =
|
|
42
|
+
type SessionStatus = "pending" | "waiting_for_wallet" | "verified" | "rejected" | "expired" | "cancelled" | "error";
|
|
43
43
|
/**
|
|
44
44
|
* Terminal states that cannot transition further.
|
|
45
45
|
*/
|
|
@@ -155,7 +155,7 @@ interface VerifyTokenInput {
|
|
|
155
155
|
interface VerifyTokenResult {
|
|
156
156
|
valid: boolean;
|
|
157
157
|
claims?: VerifiedClaims;
|
|
158
|
-
error?:
|
|
158
|
+
error?: "invalid_token" | "expired" | "already_consumed" | "invalid_signature";
|
|
159
159
|
}
|
|
160
160
|
/**
|
|
161
161
|
* API error response.
|
|
@@ -168,7 +168,7 @@ interface ApiError {
|
|
|
168
168
|
/**
|
|
169
169
|
* Operation mode for the verifier.
|
|
170
170
|
*/
|
|
171
|
-
type VerifierMode =
|
|
171
|
+
type VerifierMode = "demo" | "production";
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
174
|
* @eudi-verify/server - Key-Value Store Interface
|
package/dist/index.js
CHANGED
|
@@ -134,7 +134,11 @@ var MockEngine = class {
|
|
|
134
134
|
const requestedClaims = Object.keys(config.request).filter(
|
|
135
135
|
(k) => config.request[k] === true
|
|
136
136
|
);
|
|
137
|
-
const qrUrl = this.buildMockQrUrl(
|
|
137
|
+
const qrUrl = this.buildMockQrUrl(
|
|
138
|
+
config.sessionId,
|
|
139
|
+
config.baseUrl,
|
|
140
|
+
requestedClaims
|
|
141
|
+
);
|
|
138
142
|
return {
|
|
139
143
|
qrUrl,
|
|
140
144
|
engineData: {
|
|
@@ -274,7 +278,9 @@ var OpenEudiEngine = class {
|
|
|
274
278
|
redirect_uri: `${this.config.baseUrl}/callback`,
|
|
275
279
|
state: session.id,
|
|
276
280
|
nonce,
|
|
277
|
-
presentation_definition: this.buildPresentationDefinition(
|
|
281
|
+
presentation_definition: this.buildPresentationDefinition(
|
|
282
|
+
session.request
|
|
283
|
+
),
|
|
278
284
|
mode: "demo"
|
|
279
285
|
});
|
|
280
286
|
}
|
|
@@ -285,7 +291,9 @@ var OpenEudiEngine = class {
|
|
|
285
291
|
redirect_uri: `${this.config.baseUrl}/callback`,
|
|
286
292
|
state: session.id,
|
|
287
293
|
nonce,
|
|
288
|
-
presentation_definition: this.buildPresentationDefinition(
|
|
294
|
+
presentation_definition: this.buildPresentationDefinition(
|
|
295
|
+
session.request
|
|
296
|
+
)
|
|
289
297
|
});
|
|
290
298
|
}
|
|
291
299
|
async cancelSession(_session) {
|
|
@@ -328,7 +336,9 @@ var OpenEudiEngine = class {
|
|
|
328
336
|
return `openid4vp://authorize?${params.toString()}`;
|
|
329
337
|
}
|
|
330
338
|
buildPresentationDefinition(request) {
|
|
331
|
-
const requestedClaims = Object.keys(request).filter(
|
|
339
|
+
const requestedClaims = Object.keys(request).filter(
|
|
340
|
+
(k) => request[k] === true
|
|
341
|
+
);
|
|
332
342
|
const inputDescriptors = requestedClaims.map((claim) => ({
|
|
333
343
|
id: claim,
|
|
334
344
|
name: this.getClaimDisplayName(claim),
|