humankey 0.2.0 → 0.3.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 +165 -4
- package/dist/adapter-core-CFN3y3L0.d.ts +41 -0
- package/dist/adapter-core-CoBcSqdG.d.cts +41 -0
- package/dist/chunk-2KMBF2TH.cjs +27 -0
- package/dist/chunk-G2IT2ZP5.cjs +124 -0
- package/dist/chunk-G2X46ZRM.mjs +27 -0
- package/dist/chunk-RANGHXC7.mjs +75 -0
- package/dist/chunk-RQSBOE7B.cjs +76 -0
- package/dist/chunk-SZPXOH7Z.cjs +75 -0
- package/dist/chunk-U2AGXRNW.mjs +76 -0
- package/dist/chunk-Y6KQC7WK.mjs +124 -0
- package/dist/confirm-6RXLI2SS.cjs +9 -0
- package/dist/confirm-TSVTFC4N.mjs +9 -0
- package/dist/express.cjs +14 -102
- package/dist/express.d.cts +5 -38
- package/dist/express.d.ts +5 -38
- package/dist/express.mjs +17 -105
- package/dist/fastify.cjs +30 -0
- package/dist/fastify.d.cts +22 -0
- package/dist/fastify.d.ts +22 -0
- package/dist/fastify.mjs +30 -0
- package/dist/hono.cjs +35 -0
- package/dist/hono.d.cts +22 -0
- package/dist/hono.d.ts +22 -0
- package/dist/hono.mjs +35 -0
- package/dist/index.cjs +5 -144
- package/dist/index.mjs +10 -149
- package/dist/nextjs.cjs +40 -0
- package/dist/nextjs.d.cts +28 -0
- package/dist/nextjs.d.ts +28 -0
- package/dist/nextjs.mjs +40 -0
- package/dist/react.cjs +131 -0
- package/dist/react.d.cts +60 -0
- package/dist/react.d.ts +60 -0
- package/dist/react.mjs +131 -0
- package/dist/register-Y2ADAMHB.cjs +7 -0
- package/dist/register-ZGR2LHQ6.mjs +7 -0
- package/dist/tap-DJVQXGEJ.mjs +8 -0
- package/dist/tap-N5RN26H2.cjs +8 -0
- package/package.json +58 -2
package/README.md
CHANGED
|
@@ -25,7 +25,12 @@ humankey/
|
|
|
25
25
|
│ ├── verify.ts # Server-side proof verification (humankey/verify)
|
|
26
26
|
│ ├── registration-verify.ts # Server-side registration verification
|
|
27
27
|
│ ├── challenge.ts # Server-side challenge generation
|
|
28
|
+
│ ├── adapter-core.ts # Shared handler logic for all framework adapters
|
|
28
29
|
│ ├── express.ts # Express framework adapter (humankey/express)
|
|
30
|
+
│ ├── nextjs.ts # Next.js App Router adapter (humankey/nextjs)
|
|
31
|
+
│ ├── hono.ts # Hono adapter (humankey/hono)
|
|
32
|
+
│ ├── fastify.ts # Fastify plugin (humankey/fastify)
|
|
33
|
+
│ ├── react.ts # React hook (humankey/react)
|
|
29
34
|
│ ├── hash.ts # SHA-256 canonical JSON hashing (isomorphic)
|
|
30
35
|
│ ├── types.ts # All type definitions
|
|
31
36
|
│ └── errors.ts # Typed error classes
|
|
@@ -36,10 +41,14 @@ humankey/
|
|
|
36
41
|
└── examples/basic/ # Working Express + HTML example
|
|
37
42
|
```
|
|
38
43
|
|
|
39
|
-
**
|
|
44
|
+
**Seven entry points:**
|
|
40
45
|
- `humankey` — browser SDK (confirm + tap + register)
|
|
41
46
|
- `humankey/verify` — server-side verification, registration, and challenge generation (any JS runtime)
|
|
42
47
|
- `humankey/express` — Express router with built-in challenge lifecycle, registration, and verification
|
|
48
|
+
- `humankey/nextjs` — Next.js App Router route handlers
|
|
49
|
+
- `humankey/hono` — Hono app with humankey routes
|
|
50
|
+
- `humankey/fastify` — Fastify plugin
|
|
51
|
+
- `humankey/react` — React hook for the confirm → tap flow
|
|
43
52
|
|
|
44
53
|
## How It Works
|
|
45
54
|
|
|
@@ -61,10 +70,18 @@ npm install humankey @simplewebauthn/browser
|
|
|
61
70
|
|
|
62
71
|
`@simplewebauthn/browser` is a peer dependency (only needed in the browser).
|
|
63
72
|
|
|
64
|
-
For the
|
|
73
|
+
For framework adapters, install the framework alongside humankey:
|
|
65
74
|
|
|
66
75
|
```bash
|
|
67
|
-
npm install humankey express
|
|
76
|
+
npm install humankey express # Express
|
|
77
|
+
npm install humankey hono # Hono
|
|
78
|
+
npm install humankey fastify # Fastify
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
For the React hook:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm install humankey @simplewebauthn/browser react
|
|
68
85
|
```
|
|
69
86
|
|
|
70
87
|
## Usage
|
|
@@ -143,6 +160,146 @@ class RedisChallengeStore implements ChallengeStore {
|
|
|
143
160
|
}
|
|
144
161
|
```
|
|
145
162
|
|
|
163
|
+
### Next.js Adapter
|
|
164
|
+
|
|
165
|
+
For Next.js App Router. Each route handler is a separate file:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// app/api/humankey/challenge/route.ts
|
|
169
|
+
import { createHumanKeyHandlers } from 'humankey/nextjs';
|
|
170
|
+
import type { TapCredential } from 'humankey/verify';
|
|
171
|
+
|
|
172
|
+
const credentials = new Map<string, TapCredential>();
|
|
173
|
+
|
|
174
|
+
const hk = createHumanKeyHandlers({
|
|
175
|
+
rpID: 'example.com',
|
|
176
|
+
rpName: 'My App',
|
|
177
|
+
origin: 'https://example.com',
|
|
178
|
+
getCredential: async (id) => credentials.get(id) ?? null,
|
|
179
|
+
onRegister: async (credential) => {
|
|
180
|
+
credentials.set(credential.id, credential);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
export const POST = hk.challenge;
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// app/api/humankey/register/route.ts
|
|
189
|
+
export const POST = hk.register;
|
|
190
|
+
|
|
191
|
+
// app/api/humankey/verify/route.ts
|
|
192
|
+
export const POST = hk.verify;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Uses the Web `Request`/`Response` API — no Next.js-specific types required.
|
|
196
|
+
|
|
197
|
+
### Hono Adapter
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { Hono } from 'hono';
|
|
201
|
+
import { createHumanKeyApp } from 'humankey/hono';
|
|
202
|
+
import type { TapCredential } from 'humankey/verify';
|
|
203
|
+
|
|
204
|
+
const app = new Hono();
|
|
205
|
+
const credentials = new Map<string, TapCredential>();
|
|
206
|
+
|
|
207
|
+
app.route('/api', createHumanKeyApp({
|
|
208
|
+
rpID: 'example.com',
|
|
209
|
+
rpName: 'My App',
|
|
210
|
+
origin: 'https://example.com',
|
|
211
|
+
getCredential: async (id) => credentials.get(id) ?? null,
|
|
212
|
+
onRegister: async (credential) => {
|
|
213
|
+
credentials.set(credential.id, credential);
|
|
214
|
+
},
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
export default app;
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Fastify Adapter
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import Fastify from 'fastify';
|
|
224
|
+
import { humanKeyPlugin } from 'humankey/fastify';
|
|
225
|
+
import type { TapCredential } from 'humankey/verify';
|
|
226
|
+
|
|
227
|
+
const app = Fastify();
|
|
228
|
+
const credentials = new Map<string, TapCredential>();
|
|
229
|
+
|
|
230
|
+
app.register(humanKeyPlugin, {
|
|
231
|
+
prefix: '/api',
|
|
232
|
+
rpID: 'example.com',
|
|
233
|
+
rpName: 'My App',
|
|
234
|
+
origin: 'https://example.com',
|
|
235
|
+
getCredential: async (id) => credentials.get(id) ?? null,
|
|
236
|
+
onRegister: async (credential) => {
|
|
237
|
+
credentials.set(credential.id, credential);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
app.listen({ port: 3000 });
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### React Hook
|
|
245
|
+
|
|
246
|
+
The `useHumanKey` hook manages the full client-side flow: fetch challenge, show confirmation code, trigger hardware key tap, and verify.
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
import { useHumanKey } from 'humankey/react';
|
|
250
|
+
|
|
251
|
+
function TransferButton({ credentialId }: { credentialId: string }) {
|
|
252
|
+
const {
|
|
253
|
+
status,
|
|
254
|
+
confirmationCode,
|
|
255
|
+
error,
|
|
256
|
+
startAction,
|
|
257
|
+
confirmCode,
|
|
258
|
+
reset,
|
|
259
|
+
} = useHumanKey({ rpID: 'example.com', apiBase: '/api' });
|
|
260
|
+
|
|
261
|
+
const handleTransfer = async () => {
|
|
262
|
+
// Step 1: Start the action — fetches challenge, generates confirmation code
|
|
263
|
+
await startAction(
|
|
264
|
+
{ action: 'transfer', data: { to: 'bob', amount: 100 } },
|
|
265
|
+
[{ id: credentialId }],
|
|
266
|
+
);
|
|
267
|
+
// status is now 'confirming', confirmationCode is e.g. 'A7X3'
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const handleConfirm = async (userInput: string) => {
|
|
271
|
+
// Step 2: User typed the code — triggers YubiKey tap and server verification
|
|
272
|
+
const proof = await confirmCode(userInput);
|
|
273
|
+
// status is now 'verified', proof contains the signed assertion
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<div>
|
|
278
|
+
{status === 'idle' && <button onClick={handleTransfer}>Send $100</button>}
|
|
279
|
+
{status === 'confirming' && (
|
|
280
|
+
<div>
|
|
281
|
+
<p>Type this code: <strong>{confirmationCode}</strong></p>
|
|
282
|
+
<input onKeyDown={(e) => {
|
|
283
|
+
if (e.key === 'Enter') handleConfirm(e.currentTarget.value);
|
|
284
|
+
}} />
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
{status === 'tapping' && <p>Tap your YubiKey...</p>}
|
|
288
|
+
{status === 'verified' && <p>Transfer approved!</p>}
|
|
289
|
+
{status === 'error' && <p>Error: {error?.message} <button onClick={reset}>Retry</button></p>}
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The hook also exposes a `register` function for one-time key registration:
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
const { register } = useHumanKey({ rpID: 'example.com' });
|
|
299
|
+
const result = await register('alice');
|
|
300
|
+
// result.credentialId — store this for future use
|
|
301
|
+
```
|
|
302
|
+
|
|
146
303
|
### Server (manual — challenge + registration + verify)
|
|
147
304
|
|
|
148
305
|
```ts
|
|
@@ -310,7 +467,11 @@ For production, consider:
|
|
|
310
467
|
|---|---|---|
|
|
311
468
|
| `humankey` | Browser | `createConfirmation`, `requestTap`, `registerKey`, `isHumanKeySupported`, `hashAction`, `HumanKeyError` |
|
|
312
469
|
| `humankey/verify` | Server (any JS runtime) | `verifyTapProof`, `verifyRegistration`, `createChallenge`, `HumanKeyError` |
|
|
313
|
-
| `humankey/express` | Server (Express) | `createHumanKeyRouter`, `MemoryChallengeStore`, `ChallengeStore
|
|
470
|
+
| `humankey/express` | Server (Express) | `createHumanKeyRouter`, `MemoryChallengeStore`, `ChallengeStore` |
|
|
471
|
+
| `humankey/nextjs` | Server (Next.js App Router) | `createHumanKeyHandlers`, `MemoryChallengeStore`, `ChallengeStore` |
|
|
472
|
+
| `humankey/hono` | Server (Hono) | `createHumanKeyApp`, `MemoryChallengeStore`, `ChallengeStore` |
|
|
473
|
+
| `humankey/fastify` | Server (Fastify) | `humanKeyPlugin`, `MemoryChallengeStore`, `ChallengeStore` |
|
|
474
|
+
| `humankey/react` | Browser (React) | `useHumanKey` |
|
|
314
475
|
|
|
315
476
|
### Error Codes
|
|
316
477
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { c as TapCredential, V as VerifyResult, A as ActionPayload } from './types-By44RNIt.js';
|
|
2
|
+
|
|
3
|
+
/** Abstraction for challenge storage with TTL and single-use semantics. */
|
|
4
|
+
interface ChallengeStore {
|
|
5
|
+
/** Store a challenge with a TTL. */
|
|
6
|
+
set(id: string, challenge: string, ttlMs: number): Promise<void>;
|
|
7
|
+
/** Retrieve and delete a challenge (single-use). Returns null if expired or not found. */
|
|
8
|
+
get(id: string): Promise<string | null>;
|
|
9
|
+
}
|
|
10
|
+
/** In-memory challenge store with TTL enforcement. Use a database or Redis in production. */
|
|
11
|
+
declare class MemoryChallengeStore implements ChallengeStore {
|
|
12
|
+
private store;
|
|
13
|
+
set(id: string, challenge: string, ttlMs: number): Promise<void>;
|
|
14
|
+
get(id: string): Promise<string | null>;
|
|
15
|
+
private cleanup;
|
|
16
|
+
}
|
|
17
|
+
/** Shared configuration for all framework adapters. */
|
|
18
|
+
interface HumanKeyAdapterConfig {
|
|
19
|
+
/** Relying party ID, e.g. "example.com" */
|
|
20
|
+
rpID: string;
|
|
21
|
+
/** Relying party display name */
|
|
22
|
+
rpName: string;
|
|
23
|
+
/** Expected origin(s), e.g. "https://example.com" */
|
|
24
|
+
origin: string | string[];
|
|
25
|
+
/** Challenge TTL in ms (default: 60000) */
|
|
26
|
+
challengeTTL?: number;
|
|
27
|
+
/** Custom challenge store (default: MemoryChallengeStore) */
|
|
28
|
+
challengeStore?: ChallengeStore;
|
|
29
|
+
/** Look up a stored credential by ID */
|
|
30
|
+
getCredential: (credentialId: string) => Promise<TapCredential | null>;
|
|
31
|
+
/** Called after successful registration — store the credential */
|
|
32
|
+
onRegister: (credential: TapCredential) => Promise<void>;
|
|
33
|
+
/** Called after successful tap verification */
|
|
34
|
+
onVerify?: (result: VerifyResult, action: ActionPayload) => Promise<void>;
|
|
35
|
+
/** Require user verification (default: true) */
|
|
36
|
+
requireUserVerification?: boolean;
|
|
37
|
+
/** Restrict to specific authenticator models by AAGUID */
|
|
38
|
+
allowedAAGUIDs?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { type ChallengeStore as C, type HumanKeyAdapterConfig as H, MemoryChallengeStore as M };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { c as TapCredential, V as VerifyResult, A as ActionPayload } from './types-By44RNIt.cjs';
|
|
2
|
+
|
|
3
|
+
/** Abstraction for challenge storage with TTL and single-use semantics. */
|
|
4
|
+
interface ChallengeStore {
|
|
5
|
+
/** Store a challenge with a TTL. */
|
|
6
|
+
set(id: string, challenge: string, ttlMs: number): Promise<void>;
|
|
7
|
+
/** Retrieve and delete a challenge (single-use). Returns null if expired or not found. */
|
|
8
|
+
get(id: string): Promise<string | null>;
|
|
9
|
+
}
|
|
10
|
+
/** In-memory challenge store with TTL enforcement. Use a database or Redis in production. */
|
|
11
|
+
declare class MemoryChallengeStore implements ChallengeStore {
|
|
12
|
+
private store;
|
|
13
|
+
set(id: string, challenge: string, ttlMs: number): Promise<void>;
|
|
14
|
+
get(id: string): Promise<string | null>;
|
|
15
|
+
private cleanup;
|
|
16
|
+
}
|
|
17
|
+
/** Shared configuration for all framework adapters. */
|
|
18
|
+
interface HumanKeyAdapterConfig {
|
|
19
|
+
/** Relying party ID, e.g. "example.com" */
|
|
20
|
+
rpID: string;
|
|
21
|
+
/** Relying party display name */
|
|
22
|
+
rpName: string;
|
|
23
|
+
/** Expected origin(s), e.g. "https://example.com" */
|
|
24
|
+
origin: string | string[];
|
|
25
|
+
/** Challenge TTL in ms (default: 60000) */
|
|
26
|
+
challengeTTL?: number;
|
|
27
|
+
/** Custom challenge store (default: MemoryChallengeStore) */
|
|
28
|
+
challengeStore?: ChallengeStore;
|
|
29
|
+
/** Look up a stored credential by ID */
|
|
30
|
+
getCredential: (credentialId: string) => Promise<TapCredential | null>;
|
|
31
|
+
/** Called after successful registration — store the credential */
|
|
32
|
+
onRegister: (credential: TapCredential) => Promise<void>;
|
|
33
|
+
/** Called after successful tap verification */
|
|
34
|
+
onVerify?: (result: VerifyResult, action: ActionPayload) => Promise<void>;
|
|
35
|
+
/** Require user verification (default: true) */
|
|
36
|
+
requireUserVerification?: boolean;
|
|
37
|
+
/** Restrict to specific authenticator models by AAGUID */
|
|
38
|
+
allowedAAGUIDs?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { type ChallengeStore as C, type HumanKeyAdapterConfig as H, MemoryChallengeStore as M };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
var _chunkJI6NIMGKcjs = require('./chunk-JI6NIMGK.cjs');
|
|
7
|
+
|
|
8
|
+
// src/confirm.ts
|
|
9
|
+
async function createConfirmation(action) {
|
|
10
|
+
const hashBuffer = await _chunkJI6NIMGKcjs.hashAction.call(void 0, action);
|
|
11
|
+
const code = _chunkJI6NIMGKcjs.deriveConfirmationCode.call(void 0, hashBuffer);
|
|
12
|
+
const actionHash = _chunkJI6NIMGKcjs.bufferToBase64url.call(void 0, hashBuffer);
|
|
13
|
+
return { code, actionHash };
|
|
14
|
+
}
|
|
15
|
+
function validateConfirmation(confirmation, userInput) {
|
|
16
|
+
if (userInput.toUpperCase().trim() !== confirmation.code.toUpperCase()) {
|
|
17
|
+
throw new (0, _chunkJI6NIMGKcjs.HumanKeyError)(
|
|
18
|
+
"Confirmation code mismatch",
|
|
19
|
+
"CONFIRMATION_MISMATCH"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
exports.createConfirmation = createConfirmation; exports.validateConfirmation = validateConfirmation;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
var _chunkFW3YUVJCcjs = require('./chunk-FW3YUVJC.cjs');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
var _chunkJI6NIMGKcjs = require('./chunk-JI6NIMGK.cjs');
|
|
9
|
+
|
|
10
|
+
// src/adapter-core.ts
|
|
11
|
+
var MemoryChallengeStore = (_class = class {constructor() { _class.prototype.__init.call(this); }
|
|
12
|
+
__init() {this.store = /* @__PURE__ */ new Map()}
|
|
13
|
+
async set(id, challenge, ttlMs) {
|
|
14
|
+
this.store.set(id, { challenge, expiresAt: Date.now() + ttlMs });
|
|
15
|
+
this.cleanup();
|
|
16
|
+
}
|
|
17
|
+
async get(id) {
|
|
18
|
+
const entry = this.store.get(id);
|
|
19
|
+
if (!entry) return null;
|
|
20
|
+
this.store.delete(id);
|
|
21
|
+
if (Date.now() > entry.expiresAt) return null;
|
|
22
|
+
return entry.challenge;
|
|
23
|
+
}
|
|
24
|
+
cleanup() {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
for (const [id, entry] of this.store) {
|
|
27
|
+
if (now > entry.expiresAt) this.store.delete(id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}, _class);
|
|
31
|
+
function resolveConfig(config) {
|
|
32
|
+
return {
|
|
33
|
+
rpID: config.rpID,
|
|
34
|
+
rpName: config.rpName,
|
|
35
|
+
origin: config.origin,
|
|
36
|
+
challengeTTL: _nullishCoalesce(config.challengeTTL, () => ( 6e4)),
|
|
37
|
+
challengeStore: _nullishCoalesce(config.challengeStore, () => ( new MemoryChallengeStore())),
|
|
38
|
+
getCredential: config.getCredential,
|
|
39
|
+
onRegister: config.onRegister,
|
|
40
|
+
onVerify: config.onVerify,
|
|
41
|
+
requireUserVerification: _nullishCoalesce(config.requireUserVerification, () => ( true)),
|
|
42
|
+
allowedAAGUIDs: config.allowedAAGUIDs
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function handleChallenge(config) {
|
|
46
|
+
try {
|
|
47
|
+
const challenge = _chunkFW3YUVJCcjs.createChallenge.call(void 0, );
|
|
48
|
+
const challengeId = crypto.randomUUID();
|
|
49
|
+
await config.challengeStore.set(challengeId, challenge, config.challengeTTL);
|
|
50
|
+
return { status: 200, body: { challengeId, challenge } };
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return { status: 500, body: { error: "Internal server error" } };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function handleRegister(config, body) {
|
|
56
|
+
try {
|
|
57
|
+
const challenge = await config.challengeStore.get(body.challengeId);
|
|
58
|
+
if (!challenge) {
|
|
59
|
+
return { status: 400, body: { error: "Challenge not found or expired" } };
|
|
60
|
+
}
|
|
61
|
+
const result = await _chunkFW3YUVJCcjs.verifyRegistration.call(void 0, {
|
|
62
|
+
response: body.response,
|
|
63
|
+
expectedChallenge: challenge,
|
|
64
|
+
expectedOrigin: config.origin,
|
|
65
|
+
expectedRPID: config.rpID,
|
|
66
|
+
requireUserVerification: config.requireUserVerification,
|
|
67
|
+
allowedAAGUIDs: config.allowedAAGUIDs
|
|
68
|
+
});
|
|
69
|
+
await config.onRegister(result.credential);
|
|
70
|
+
return { status: 200, body: { ok: true, credentialId: result.credential.id } };
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return errorResult(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function handleVerify(config, body) {
|
|
76
|
+
try {
|
|
77
|
+
const challenge = await config.challengeStore.get(body.challengeId);
|
|
78
|
+
if (!challenge) {
|
|
79
|
+
return { status: 400, body: { error: "Challenge not found or expired" } };
|
|
80
|
+
}
|
|
81
|
+
const proof = body.proof;
|
|
82
|
+
const credential = await config.getCredential(_optionalChain([proof, 'access', _ => _.response, 'optionalAccess', _2 => _2.id]));
|
|
83
|
+
if (!credential) {
|
|
84
|
+
return { status: 400, body: { error: "Credential not found" } };
|
|
85
|
+
}
|
|
86
|
+
const result = await _chunkFW3YUVJCcjs.verifyTapProof.call(void 0, {
|
|
87
|
+
proof,
|
|
88
|
+
credential,
|
|
89
|
+
expectedChallenge: challenge,
|
|
90
|
+
expectedAction: body.action,
|
|
91
|
+
expectedOrigin: config.origin,
|
|
92
|
+
expectedRPID: config.rpID,
|
|
93
|
+
requireUserVerification: config.requireUserVerification
|
|
94
|
+
});
|
|
95
|
+
credential.counter = result.newCounter;
|
|
96
|
+
if (config.onVerify) {
|
|
97
|
+
await config.onVerify(result, body.action);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
status: 200,
|
|
101
|
+
body: {
|
|
102
|
+
verified: result.verified,
|
|
103
|
+
confirmationValid: result.confirmationValid,
|
|
104
|
+
userVerified: result.userVerified
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return errorResult(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function errorResult(error) {
|
|
112
|
+
if (error instanceof _chunkJI6NIMGKcjs.HumanKeyError) {
|
|
113
|
+
return { status: 400, body: { error: error.message, code: error.code } };
|
|
114
|
+
}
|
|
115
|
+
return { status: 500, body: { error: "Internal server error" } };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
exports.MemoryChallengeStore = MemoryChallengeStore; exports.resolveConfig = resolveConfig; exports.handleChallenge = handleChallenge; exports.handleRegister = handleRegister; exports.handleVerify = handleVerify;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HumanKeyError,
|
|
3
|
+
bufferToBase64url,
|
|
4
|
+
deriveConfirmationCode,
|
|
5
|
+
hashAction
|
|
6
|
+
} from "./chunk-CLQSCDXC.mjs";
|
|
7
|
+
|
|
8
|
+
// src/confirm.ts
|
|
9
|
+
async function createConfirmation(action) {
|
|
10
|
+
const hashBuffer = await hashAction(action);
|
|
11
|
+
const code = deriveConfirmationCode(hashBuffer);
|
|
12
|
+
const actionHash = bufferToBase64url(hashBuffer);
|
|
13
|
+
return { code, actionHash };
|
|
14
|
+
}
|
|
15
|
+
function validateConfirmation(confirmation, userInput) {
|
|
16
|
+
if (userInput.toUpperCase().trim() !== confirmation.code.toUpperCase()) {
|
|
17
|
+
throw new HumanKeyError(
|
|
18
|
+
"Confirmation code mismatch",
|
|
19
|
+
"CONFIRMATION_MISMATCH"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
createConfirmation,
|
|
26
|
+
validateConfirmation
|
|
27
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateConfirmation
|
|
3
|
+
} from "./chunk-G2X46ZRM.mjs";
|
|
4
|
+
import {
|
|
5
|
+
HumanKeyError,
|
|
6
|
+
base64urlToBuffer,
|
|
7
|
+
bufferToBase64url,
|
|
8
|
+
combineAndHash,
|
|
9
|
+
hashAction
|
|
10
|
+
} from "./chunk-CLQSCDXC.mjs";
|
|
11
|
+
|
|
12
|
+
// src/tap.ts
|
|
13
|
+
import { startAuthentication } from "@simplewebauthn/browser";
|
|
14
|
+
async function requestTap(request) {
|
|
15
|
+
const {
|
|
16
|
+
challenge,
|
|
17
|
+
action,
|
|
18
|
+
confirmation,
|
|
19
|
+
userInput,
|
|
20
|
+
allowCredentials,
|
|
21
|
+
rpID,
|
|
22
|
+
userVerification = "required",
|
|
23
|
+
timeout = 6e4
|
|
24
|
+
} = request;
|
|
25
|
+
validateConfirmation(confirmation, userInput);
|
|
26
|
+
const actionHashBuffer = await hashAction(action);
|
|
27
|
+
const confirmationInput = `${confirmation.code}:${userInput.toUpperCase().trim()}`;
|
|
28
|
+
const confirmationHashBuffer = await crypto.subtle.digest(
|
|
29
|
+
"SHA-256",
|
|
30
|
+
new TextEncoder().encode(confirmationInput)
|
|
31
|
+
);
|
|
32
|
+
const challengeBuffer = base64urlToBuffer(challenge);
|
|
33
|
+
const finalChallenge = await combineAndHash(
|
|
34
|
+
challengeBuffer,
|
|
35
|
+
actionHashBuffer,
|
|
36
|
+
confirmationHashBuffer
|
|
37
|
+
);
|
|
38
|
+
const actionHash = bufferToBase64url(actionHashBuffer);
|
|
39
|
+
const confirmationHash = bufferToBase64url(confirmationHashBuffer);
|
|
40
|
+
try {
|
|
41
|
+
const response = await startAuthentication({
|
|
42
|
+
optionsJSON: {
|
|
43
|
+
challenge: bufferToBase64url(finalChallenge),
|
|
44
|
+
rpId: rpID,
|
|
45
|
+
allowCredentials: allowCredentials.map((cred) => ({
|
|
46
|
+
id: cred.id,
|
|
47
|
+
type: "public-key",
|
|
48
|
+
transports: cred.transports
|
|
49
|
+
})),
|
|
50
|
+
userVerification,
|
|
51
|
+
timeout
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
response,
|
|
56
|
+
action,
|
|
57
|
+
actionHash,
|
|
58
|
+
confirmationHash,
|
|
59
|
+
userInput: userInput.toUpperCase().trim()
|
|
60
|
+
};
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof Error && error.name === "NotAllowedError") {
|
|
63
|
+
throw new HumanKeyError(
|
|
64
|
+
"User cancelled the hardware key tap",
|
|
65
|
+
"USER_CANCELLED",
|
|
66
|
+
error
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
requestTap
|
|
75
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
var _chunkJI6NIMGKcjs = require('./chunk-JI6NIMGK.cjs');
|
|
5
|
+
|
|
6
|
+
// src/register.ts
|
|
7
|
+
var _browser = require('@simplewebauthn/browser');
|
|
8
|
+
async function registerKey(request) {
|
|
9
|
+
const {
|
|
10
|
+
challenge,
|
|
11
|
+
rpID,
|
|
12
|
+
rpName,
|
|
13
|
+
userName,
|
|
14
|
+
userDisplayName = userName,
|
|
15
|
+
excludeCredentials = [],
|
|
16
|
+
attestation = "direct",
|
|
17
|
+
userVerification = "required",
|
|
18
|
+
timeout = 6e4
|
|
19
|
+
} = request;
|
|
20
|
+
const userIdBytes = new TextEncoder().encode(userName);
|
|
21
|
+
const userIdHash = await crypto.subtle.digest("SHA-256", userIdBytes);
|
|
22
|
+
const userId = _chunkJI6NIMGKcjs.bufferToBase64url.call(void 0, userIdHash);
|
|
23
|
+
try {
|
|
24
|
+
const response = await _browser.startRegistration.call(void 0, {
|
|
25
|
+
optionsJSON: {
|
|
26
|
+
rp: { name: rpName, id: rpID },
|
|
27
|
+
user: {
|
|
28
|
+
id: userId,
|
|
29
|
+
name: userName,
|
|
30
|
+
displayName: userDisplayName
|
|
31
|
+
},
|
|
32
|
+
challenge,
|
|
33
|
+
pubKeyCredParams: [
|
|
34
|
+
{ alg: -7, type: "public-key" },
|
|
35
|
+
// ES256
|
|
36
|
+
{ alg: -257, type: "public-key" }
|
|
37
|
+
// RS256
|
|
38
|
+
],
|
|
39
|
+
timeout,
|
|
40
|
+
attestation,
|
|
41
|
+
excludeCredentials: excludeCredentials.map((cred) => ({
|
|
42
|
+
id: cred.id,
|
|
43
|
+
type: "public-key",
|
|
44
|
+
transports: cred.transports
|
|
45
|
+
})),
|
|
46
|
+
authenticatorSelection: {
|
|
47
|
+
authenticatorAttachment: "cross-platform",
|
|
48
|
+
residentKey: "preferred",
|
|
49
|
+
userVerification
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
credentialId: response.id,
|
|
55
|
+
response,
|
|
56
|
+
transports: response.response.transports
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof Error && error.name === "NotAllowedError") {
|
|
60
|
+
throw new (0, _chunkJI6NIMGKcjs.HumanKeyError)(
|
|
61
|
+
"User cancelled hardware key registration",
|
|
62
|
+
"USER_CANCELLED",
|
|
63
|
+
error
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
throw new (0, _chunkJI6NIMGKcjs.HumanKeyError)(
|
|
67
|
+
`Registration failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
68
|
+
"REGISTRATION_FAILED",
|
|
69
|
+
error
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
exports.registerKey = registerKey;
|