@opensyber/tokenforge 0.1.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 +137 -0
- package/dist/adapters/express.d.ts +48 -0
- package/dist/adapters/express.d.ts.map +1 -0
- package/dist/adapters/express.js +73 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/fastify.d.ts +44 -0
- package/dist/adapters/fastify.d.ts.map +1 -0
- package/dist/adapters/fastify.js +69 -0
- package/dist/adapters/fastify.js.map +1 -0
- package/dist/adapters/hono.d.ts +8 -0
- package/dist/adapters/hono.d.ts.map +1 -0
- package/dist/adapters/hono.js +8 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +5 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/nextjs.d.ts +51 -0
- package/dist/adapters/nextjs.d.ts.map +1 -0
- package/dist/adapters/nextjs.js +101 -0
- package/dist/adapters/nextjs.js.map +1 -0
- package/dist/client/binding.d.ts +14 -0
- package/dist/client/binding.d.ts.map +1 -0
- package/dist/client/binding.js +49 -0
- package/dist/client/binding.js.map +1 -0
- package/dist/client/crypto.d.ts +16 -0
- package/dist/client/crypto.d.ts.map +1 -0
- package/dist/client/crypto.js +22 -0
- package/dist/client/crypto.js.map +1 -0
- package/dist/client/index.d.ts +38 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +108 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/interceptor.d.ts +13 -0
- package/dist/client/interceptor.d.ts.map +1 -0
- package/dist/client/interceptor.js +85 -0
- package/dist/client/interceptor.js.map +1 -0
- package/dist/client/signer.d.ts +15 -0
- package/dist/client/signer.d.ts.map +1 -0
- package/dist/client/signer.js +31 -0
- package/dist/client/signer.js.map +1 -0
- package/dist/client/storage.d.ts +17 -0
- package/dist/client/storage.d.ts.map +1 -0
- package/dist/client/storage.js +58 -0
- package/dist/client/storage.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/provider.d.ts +30 -0
- package/dist/react/provider.d.ts.map +1 -0
- package/dist/react/provider.js +59 -0
- package/dist/react/provider.js.map +1 -0
- package/dist/server/binding.d.ts +19 -0
- package/dist/server/binding.d.ts.map +1 -0
- package/dist/server/binding.js +87 -0
- package/dist/server/binding.js.map +1 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +6 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware.d.ts +30 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +107 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/step-up.d.ts +19 -0
- package/dist/server/step-up.d.ts.map +1 -0
- package/dist/server/step-up.js +139 -0
- package/dist/server/step-up.js.map +1 -0
- package/dist/server/storage/index.d.ts +3 -0
- package/dist/server/storage/index.d.ts.map +1 -0
- package/dist/server/storage/index.js +2 -0
- package/dist/server/storage/index.js.map +1 -0
- package/dist/server/storage/interface.d.ts +61 -0
- package/dist/server/storage/interface.d.ts.map +1 -0
- package/dist/server/storage/interface.js +2 -0
- package/dist/server/storage/interface.js.map +1 -0
- package/dist/server/storage/memory.d.ts +40 -0
- package/dist/server/storage/memory.d.ts.map +1 -0
- package/dist/server/storage/memory.js +107 -0
- package/dist/server/storage/memory.js.map +1 -0
- package/dist/server/totp.d.ts +53 -0
- package/dist/server/totp.d.ts.map +1 -0
- package/dist/server/totp.js +114 -0
- package/dist/server/totp.js.map +1 -0
- package/dist/shared/types-internal.d.ts +66 -0
- package/dist/shared/types-internal.d.ts.map +1 -0
- package/dist/shared/types-internal.js +4 -0
- package/dist/shared/types-internal.js.map +1 -0
- package/dist/shared/types.d.ts +148 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +9 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +118 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory storage for testing and development.
|
|
3
|
+
* Not suitable for production — data is lost on restart.
|
|
4
|
+
*/
|
|
5
|
+
export class MemoryStorage {
|
|
6
|
+
sessions = [];
|
|
7
|
+
events = [];
|
|
8
|
+
challenges = [];
|
|
9
|
+
nonces = new Map(); // nonce → expiry timestamp
|
|
10
|
+
otps = new Map();
|
|
11
|
+
async getSession(sessionId, deviceId) {
|
|
12
|
+
return this.sessions.find((s) => s.session_id === sessionId && s.id === deviceId && s.revoked === 0) ?? null;
|
|
13
|
+
}
|
|
14
|
+
async createSession(session) {
|
|
15
|
+
this.sessions.push({ ...session });
|
|
16
|
+
}
|
|
17
|
+
async updateTrustScore(deviceId, trustScore) {
|
|
18
|
+
const s = this.sessions.find((s) => s.id === deviceId);
|
|
19
|
+
if (s) {
|
|
20
|
+
s.trust_score = trustScore;
|
|
21
|
+
s.last_verified_at = new Date().toISOString();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async revokeSession(deviceId, reason) {
|
|
25
|
+
const s = this.sessions.find((s) => s.id === deviceId);
|
|
26
|
+
if (s) {
|
|
27
|
+
s.revoked = 1;
|
|
28
|
+
s.revoked_reason = reason;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async revokeUserSessions(userId) {
|
|
32
|
+
for (const s of this.sessions) {
|
|
33
|
+
if (s.user_id === userId && s.revoked === 0) {
|
|
34
|
+
s.revoked = 1;
|
|
35
|
+
s.revoked_reason = 'rebind';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async listUserSessions(userId, limit) {
|
|
40
|
+
return this.sessions
|
|
41
|
+
.filter((s) => s.user_id === userId)
|
|
42
|
+
.sort((a, b) => b.bound_at.localeCompare(a.bound_at))
|
|
43
|
+
.slice(0, limit);
|
|
44
|
+
}
|
|
45
|
+
async restoreTrust(deviceId, userId) {
|
|
46
|
+
const s = this.sessions.find((s) => s.id === deviceId && s.user_id === userId);
|
|
47
|
+
if (s)
|
|
48
|
+
s.trust_score = 100;
|
|
49
|
+
}
|
|
50
|
+
async hasNonce(nonce) {
|
|
51
|
+
const expiry = this.nonces.get(nonce);
|
|
52
|
+
if (!expiry)
|
|
53
|
+
return false;
|
|
54
|
+
if (Date.now() > expiry) {
|
|
55
|
+
this.nonces.delete(nonce);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
async storeNonce(nonce, ttlSeconds) {
|
|
61
|
+
this.nonces.set(nonce, Date.now() + ttlSeconds * 1000);
|
|
62
|
+
}
|
|
63
|
+
async logEvent(event) {
|
|
64
|
+
this.events.push({ ...event, created_at: new Date().toISOString() });
|
|
65
|
+
}
|
|
66
|
+
async listEvents(userId, limit, offset) {
|
|
67
|
+
return this.events
|
|
68
|
+
.filter((e) => e.userId === userId)
|
|
69
|
+
.sort((a, b) => b.created_at.localeCompare(a.created_at))
|
|
70
|
+
.slice(offset, offset + limit);
|
|
71
|
+
}
|
|
72
|
+
async createChallenge(challenge) {
|
|
73
|
+
this.challenges.push({ ...challenge });
|
|
74
|
+
}
|
|
75
|
+
async getChallenge(challengeId, userId) {
|
|
76
|
+
return this.challenges.find((c) => c.id === challengeId && c.userId === userId && c.status === 'pending') ?? null;
|
|
77
|
+
}
|
|
78
|
+
async updateChallengeStatus(challengeId, status, completedAt) {
|
|
79
|
+
const c = this.challenges.find((c) => c.id === challengeId);
|
|
80
|
+
if (c) {
|
|
81
|
+
c.status = status;
|
|
82
|
+
if (completedAt)
|
|
83
|
+
c.completedAt = completedAt;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async countRecentChallenges(userId, windowMinutes) {
|
|
87
|
+
const cutoff = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();
|
|
88
|
+
return this.challenges.filter((c) => c.userId === userId && c.createdAt > cutoff).length;
|
|
89
|
+
}
|
|
90
|
+
async storeOtp(challengeId, code, ttlSeconds) {
|
|
91
|
+
this.otps.set(challengeId, { code, expiresAt: Date.now() + ttlSeconds * 1000 });
|
|
92
|
+
}
|
|
93
|
+
async getOtp(challengeId) {
|
|
94
|
+
const entry = this.otps.get(challengeId);
|
|
95
|
+
if (!entry)
|
|
96
|
+
return null;
|
|
97
|
+
if (Date.now() > entry.expiresAt) {
|
|
98
|
+
this.otps.delete(challengeId);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return entry.code;
|
|
102
|
+
}
|
|
103
|
+
async deleteOtp(challengeId) {
|
|
104
|
+
this.otps.delete(challengeId);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../../../src/server/storage/memory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { TokenForgeServerOptions } from '../shared/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Callback surface for TOTP attempt rate-limiting. Callers plug in a
|
|
4
|
+
* storage-backed counter (KV, Redis, D1); when absent, lockout is
|
|
5
|
+
* disabled and the caller MUST enforce rate-limiting elsewhere.
|
|
6
|
+
*
|
|
7
|
+
* - `getFailedAttempts(userId)` returns the number of failed TOTP
|
|
8
|
+
* verifications for the user within the current lockout window.
|
|
9
|
+
* - `recordFailedAttempt(userId)` atomically increments the counter
|
|
10
|
+
* and sets a TTL of `windowSeconds`.
|
|
11
|
+
* - `resetFailedAttempts(userId)` clears the counter on a successful
|
|
12
|
+
* verification.
|
|
13
|
+
*/
|
|
14
|
+
export interface TotpRateLimiter {
|
|
15
|
+
getFailedAttempts(userId: string): Promise<number>;
|
|
16
|
+
recordFailedAttempt(userId: string, windowSeconds: number): Promise<void>;
|
|
17
|
+
resetFailedAttempts(userId: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare const TOTP_MAX_ATTEMPTS = 5;
|
|
20
|
+
export declare const TOTP_LOCKOUT_WINDOW_SECONDS = 300;
|
|
21
|
+
export declare class TotpLockedOutError extends Error {
|
|
22
|
+
readonly retryAfterSeconds: number;
|
|
23
|
+
constructor(retryAfterSeconds: number);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* TOTP verification (RFC 6238) using HMAC-SHA1.
|
|
27
|
+
* Time step: 30 seconds, window: +/-1 step tolerance.
|
|
28
|
+
*
|
|
29
|
+
* Rate-limiting: when `options.rateLimiter` is provided, the user is
|
|
30
|
+
* locked out for TOTP_LOCKOUT_WINDOW_SECONDS after TOTP_MAX_ATTEMPTS
|
|
31
|
+
* failures. A TotpLockedOutError is thrown while locked so callers can
|
|
32
|
+
* return a distinct 429 to the client instead of a generic "invalid
|
|
33
|
+
* code". A successful verification resets the counter immediately.
|
|
34
|
+
*
|
|
35
|
+
* @param code - The 6-digit TOTP code from the user.
|
|
36
|
+
* @param userId - User whose TOTP secret to look up.
|
|
37
|
+
* @param options - Server options; must include `getTotpSecret` callback.
|
|
38
|
+
* @returns True if the code is valid for the current or adjacent time window.
|
|
39
|
+
* @throws TotpLockedOutError if the user is currently locked out.
|
|
40
|
+
*/
|
|
41
|
+
export declare function verifyTotp(code: string, userId: string, options: TokenForgeServerOptions & {
|
|
42
|
+
getTotpSecret?: (userId: string) => Promise<string | null>;
|
|
43
|
+
rateLimiter?: TotpRateLimiter;
|
|
44
|
+
}): Promise<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* Generate a TOTP code for a given counter value.
|
|
47
|
+
* Uses HMAC-SHA1 per RFC 4226, with 6-digit output.
|
|
48
|
+
* @param secretBase32 - Base32-encoded shared secret.
|
|
49
|
+
* @param counter - HMAC counter value (typically `floor(time / 30)`).
|
|
50
|
+
* @returns A 6-digit zero-padded TOTP code.
|
|
51
|
+
*/
|
|
52
|
+
export declare function generateTotp(secretBase32: string, counter: number): Promise<string>;
|
|
53
|
+
//# sourceMappingURL=totp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"totp.d.ts","sourceRoot":"","sources":["../../src/server/totp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAElE;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,eAAe;IAC9B,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C,qBAAa,kBAAmB,SAAQ,KAAK;IAC/B,QAAQ,CAAC,iBAAiB,EAAE,MAAM;gBAAzB,iBAAiB,EAAE,MAAM;CAI/C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,uBAAuB,GAAG;IACjC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3D,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,GACA,OAAO,CAAC,OAAO,CAAC,CAgClB;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBzF"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export const TOTP_MAX_ATTEMPTS = 5;
|
|
2
|
+
export const TOTP_LOCKOUT_WINDOW_SECONDS = 300; // 5 min — matches the OWASP ASVS V6.2.6 guidance
|
|
3
|
+
export class TotpLockedOutError extends Error {
|
|
4
|
+
retryAfterSeconds;
|
|
5
|
+
constructor(retryAfterSeconds) {
|
|
6
|
+
super(`Too many failed TOTP attempts; locked out for ${retryAfterSeconds}s`);
|
|
7
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
8
|
+
this.name = 'TotpLockedOutError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* TOTP verification (RFC 6238) using HMAC-SHA1.
|
|
13
|
+
* Time step: 30 seconds, window: +/-1 step tolerance.
|
|
14
|
+
*
|
|
15
|
+
* Rate-limiting: when `options.rateLimiter` is provided, the user is
|
|
16
|
+
* locked out for TOTP_LOCKOUT_WINDOW_SECONDS after TOTP_MAX_ATTEMPTS
|
|
17
|
+
* failures. A TotpLockedOutError is thrown while locked so callers can
|
|
18
|
+
* return a distinct 429 to the client instead of a generic "invalid
|
|
19
|
+
* code". A successful verification resets the counter immediately.
|
|
20
|
+
*
|
|
21
|
+
* @param code - The 6-digit TOTP code from the user.
|
|
22
|
+
* @param userId - User whose TOTP secret to look up.
|
|
23
|
+
* @param options - Server options; must include `getTotpSecret` callback.
|
|
24
|
+
* @returns True if the code is valid for the current or adjacent time window.
|
|
25
|
+
* @throws TotpLockedOutError if the user is currently locked out.
|
|
26
|
+
*/
|
|
27
|
+
export async function verifyTotp(code, userId, options) {
|
|
28
|
+
if (!options.getTotpSecret)
|
|
29
|
+
return false;
|
|
30
|
+
const limiter = options.rateLimiter;
|
|
31
|
+
if (limiter) {
|
|
32
|
+
const attempts = await limiter.getFailedAttempts(userId);
|
|
33
|
+
if (attempts >= TOTP_MAX_ATTEMPTS) {
|
|
34
|
+
throw new TotpLockedOutError(TOTP_LOCKOUT_WINDOW_SECONDS);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const secret = await options.getTotpSecret(userId);
|
|
38
|
+
if (!secret) {
|
|
39
|
+
if (limiter)
|
|
40
|
+
await limiter.recordFailedAttempt(userId, TOTP_LOCKOUT_WINDOW_SECONDS);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const now = Math.floor(Date.now() / 1000);
|
|
44
|
+
const timeStep = 30;
|
|
45
|
+
// Check current window and ±1 step for clock skew tolerance
|
|
46
|
+
for (const offset of [-1, 0, 1]) {
|
|
47
|
+
const counter = Math.floor((now + offset * timeStep) / timeStep);
|
|
48
|
+
const expected = await generateTotp(secret, counter);
|
|
49
|
+
if (timingSafeEqual(code, expected)) {
|
|
50
|
+
if (limiter)
|
|
51
|
+
await limiter.resetFailedAttempts(userId);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (limiter)
|
|
56
|
+
await limiter.recordFailedAttempt(userId, TOTP_LOCKOUT_WINDOW_SECONDS);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Generate a TOTP code for a given counter value.
|
|
61
|
+
* Uses HMAC-SHA1 per RFC 4226, with 6-digit output.
|
|
62
|
+
* @param secretBase32 - Base32-encoded shared secret.
|
|
63
|
+
* @param counter - HMAC counter value (typically `floor(time / 30)`).
|
|
64
|
+
* @returns A 6-digit zero-padded TOTP code.
|
|
65
|
+
*/
|
|
66
|
+
export async function generateTotp(secretBase32, counter) {
|
|
67
|
+
const keyBytes = base32Decode(secretBase32);
|
|
68
|
+
const counterBytes = new ArrayBuffer(8);
|
|
69
|
+
const view = new DataView(counterBytes);
|
|
70
|
+
view.setBigUint64(0, BigInt(counter));
|
|
71
|
+
const key = await crypto.subtle.importKey('raw', keyBytes.buffer, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
|
|
72
|
+
const hmac = await crypto.subtle.sign('HMAC', key, counterBytes);
|
|
73
|
+
const hmacArray = new Uint8Array(hmac);
|
|
74
|
+
// Dynamic truncation (RFC 4226 section 5.3)
|
|
75
|
+
const offset = hmacArray[hmacArray.length - 1] & 0x0f;
|
|
76
|
+
const binary = ((hmacArray[offset] & 0x7f) << 24) |
|
|
77
|
+
((hmacArray[offset + 1] & 0xff) << 16) |
|
|
78
|
+
((hmacArray[offset + 2] & 0xff) << 8) |
|
|
79
|
+
(hmacArray[offset + 3] & 0xff);
|
|
80
|
+
return (binary % 1000000).toString().padStart(6, '0');
|
|
81
|
+
}
|
|
82
|
+
/** Decode a base32-encoded string (RFC 4648, no padding required) */
|
|
83
|
+
function base32Decode(input) {
|
|
84
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
85
|
+
const clean = input.replace(/[= ]/g, '').toUpperCase();
|
|
86
|
+
const bits = [];
|
|
87
|
+
for (const char of clean) {
|
|
88
|
+
const val = alphabet.indexOf(char);
|
|
89
|
+
if (val === -1)
|
|
90
|
+
continue;
|
|
91
|
+
for (let i = 4; i >= 0; i--) {
|
|
92
|
+
bits.push((val >> i) & 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const bytes = new Uint8Array(Math.floor(bits.length / 8));
|
|
96
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
97
|
+
let byte = 0;
|
|
98
|
+
for (let j = 0; j < 8; j++) {
|
|
99
|
+
byte = (byte << 1) | (bits[i * 8 + j] ?? 0);
|
|
100
|
+
}
|
|
101
|
+
bytes[i] = byte;
|
|
102
|
+
}
|
|
103
|
+
return bytes;
|
|
104
|
+
}
|
|
105
|
+
/** Constant-time string comparison to prevent timing attacks */
|
|
106
|
+
function timingSafeEqual(a, b) {
|
|
107
|
+
const maxLen = Math.max(a.length, b.length);
|
|
108
|
+
let result = a.length ^ b.length; // length mismatch contributes to failure
|
|
109
|
+
for (let i = 0; i < maxLen; i++) {
|
|
110
|
+
result |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
111
|
+
}
|
|
112
|
+
return result === 0;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=totp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"totp.js","sourceRoot":"","sources":["../../src/server/totp.ts"],"names":[],"mappings":"AAoBA,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAC,CAAC,iDAAiD;AAEjG,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACtB;IAArB,YAAqB,iBAAyB;QAC5C,KAAK,CAAC,iDAAiD,iBAAiB,GAAG,CAAC,CAAC;QAD1D,sBAAiB,GAAjB,iBAAiB,CAAQ;QAE5C,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,MAAc,EACd,OAGC;IAED,IAAI,CAAC,OAAO,CAAC,aAAa;QAAE,OAAO,KAAK,CAAC;IAEzC,MAAM,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IACpC,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC;QACzD,IAAI,QAAQ,IAAI,iBAAiB,EAAE,CAAC;YAClC,MAAM,IAAI,kBAAkB,CAAC,2BAA2B,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,IAAI,OAAO;YAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;QACpF,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,EAAE,CAAC;IAEpB,4DAA4D;IAC5D,KAAK,MAAM,MAAM,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,MAAM,GAAG,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrD,IAAI,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;YACpC,IAAI,OAAO;gBAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC;YACvD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,IAAI,OAAO;QAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;IACpF,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,YAAoB,EAAE,OAAe;IACtE,MAAM,QAAQ,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IAC5C,MAAM,YAAY,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,YAAY,CAAC,CAAC;IACxC,IAAI,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAEtC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACvC,KAAK,EAAE,QAAQ,CAAC,MAAqB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,CACxF,CAAC;IACF,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC;IACjE,MAAM,SAAS,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;IAEvC,4CAA4C;IAC5C,MAAM,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAE,GAAG,IAAI,CAAC;IACvD,MAAM,MAAM,GACV,CAAC,CAAC,SAAS,CAAC,MAAM,CAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACnC,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACvC,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAE,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QACtC,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAE,GAAG,IAAI,CAAC,CAAC;IAElC,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACxD,CAAC;AAED,qEAAqE;AACrE,SAAS,YAAY,CAAC,KAAa;IACjC,MAAM,QAAQ,GAAG,kCAAkC,CAAC;IACpD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACvD,MAAM,IAAI,GAAa,EAAE,CAAC;IAE1B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,SAAS;QACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IAC1D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC9C,CAAC;QACD,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,gEAAgE;AAChE,SAAS,eAAe,CAAC,CAAS,EAAE,CAAS;IAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,MAAM,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,yCAAyC;IAC3E,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,MAAM,KAAK,CAAC,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Step-up challenge stored in D1 */
|
|
2
|
+
export interface StepUpChallenge {
|
|
3
|
+
id: string;
|
|
4
|
+
session_id: string;
|
|
5
|
+
user_id: string;
|
|
6
|
+
reason: string;
|
|
7
|
+
status: 'pending' | 'completed' | 'expired' | 'failed';
|
|
8
|
+
method: 'totp' | 'email_otp' | 'passkey' | null;
|
|
9
|
+
created_at: string;
|
|
10
|
+
expires_at: string;
|
|
11
|
+
completed_at: string | null;
|
|
12
|
+
}
|
|
13
|
+
/** Device metadata collected during binding */
|
|
14
|
+
export interface DeviceMetadata {
|
|
15
|
+
userAgent: string;
|
|
16
|
+
language: string;
|
|
17
|
+
platform: string;
|
|
18
|
+
screenResolution: string;
|
|
19
|
+
timezone: string;
|
|
20
|
+
colorDepth: number;
|
|
21
|
+
}
|
|
22
|
+
/** Stored device key in IndexedDB */
|
|
23
|
+
export interface StoredDevice {
|
|
24
|
+
deviceId: string;
|
|
25
|
+
keyPair: CryptoKeyPair;
|
|
26
|
+
createdAt: number;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
}
|
|
29
|
+
/** Binding request sent to server */
|
|
30
|
+
export interface BindRequest {
|
|
31
|
+
publicKey: JsonWebKey;
|
|
32
|
+
sessionId: string;
|
|
33
|
+
metadata: DeviceMetadata;
|
|
34
|
+
}
|
|
35
|
+
/** Binding response from server */
|
|
36
|
+
export interface BindResponse {
|
|
37
|
+
deviceId: string;
|
|
38
|
+
expiresAt: string;
|
|
39
|
+
}
|
|
40
|
+
/** Trust score response from server */
|
|
41
|
+
export interface TrustScoreResponse {
|
|
42
|
+
trustScore: number;
|
|
43
|
+
isBound: boolean;
|
|
44
|
+
deviceId: string | null;
|
|
45
|
+
}
|
|
46
|
+
/** Step-up initiation request */
|
|
47
|
+
export interface StepUpInitiateRequest {
|
|
48
|
+
method: 'totp' | 'email_otp' | 'passkey';
|
|
49
|
+
}
|
|
50
|
+
/** Step-up initiation response */
|
|
51
|
+
export interface StepUpInitiateResponse {
|
|
52
|
+
challengeId: string;
|
|
53
|
+
method: string;
|
|
54
|
+
expiresAt: string;
|
|
55
|
+
}
|
|
56
|
+
/** Step-up completion request */
|
|
57
|
+
export interface StepUpCompleteRequest {
|
|
58
|
+
challengeId: string;
|
|
59
|
+
code: string;
|
|
60
|
+
}
|
|
61
|
+
/** Step-up completion response */
|
|
62
|
+
export interface StepUpCompleteResponse {
|
|
63
|
+
verified: boolean;
|
|
64
|
+
trustScore: number;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=types-internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-internal.d.ts","sourceRoot":"","sources":["../../src/shared/types-internal.ts"],"names":[],"mappings":"AAGA,qCAAqC;AACrC,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;IACvD,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,IAAI,CAAC;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,+CAA+C;AAC/C,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qCAAqC;AACrC,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,aAAa,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qCAAqC;AACrC,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,UAAU,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,mCAAmC;AACnC,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,iCAAiC;AACjC,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;CAC1C;AAED,kCAAkC;AAClC,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iCAAiC;AACjC,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,kCAAkC;AAClC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-internal.js","sourceRoot":"","sources":["../../src/shared/types-internal.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,sEAAsE"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Configuration for the TokenForge client SDK */
|
|
2
|
+
export interface TokenForgeConfig {
|
|
3
|
+
/** Base URL of the TokenForge-protected API */
|
|
4
|
+
apiBase: string;
|
|
5
|
+
/** How to get the current session ID (from your auth provider) */
|
|
6
|
+
getSessionId: () => string | null;
|
|
7
|
+
/** Called when server demands step-up authentication */
|
|
8
|
+
onStepUpRequired?: (reason: string) => void;
|
|
9
|
+
/** Called when session is revoked by server */
|
|
10
|
+
onSessionRevoked?: () => void;
|
|
11
|
+
/** Called on binding success */
|
|
12
|
+
onDeviceBound?: (deviceId: string) => void;
|
|
13
|
+
/** Custom header names */
|
|
14
|
+
headers?: {
|
|
15
|
+
signature?: string;
|
|
16
|
+
nonce?: string;
|
|
17
|
+
timestamp?: string;
|
|
18
|
+
deviceId?: string;
|
|
19
|
+
};
|
|
20
|
+
/** Enable automatic fetch interception (default: true) */
|
|
21
|
+
autoIntercept?: boolean;
|
|
22
|
+
/** Paths to skip (no signing needed) */
|
|
23
|
+
skipPaths?: string[];
|
|
24
|
+
}
|
|
25
|
+
/** Configuration for the TokenForge server middleware */
|
|
26
|
+
export interface TokenForgeServerOptions {
|
|
27
|
+
storage: TokenForgeStorageRef;
|
|
28
|
+
trustThresholds: {
|
|
29
|
+
/** Score >= this: request proceeds (default: 80) */
|
|
30
|
+
allow: number;
|
|
31
|
+
/** Score >= this: step-up required (default: 40) */
|
|
32
|
+
stepUp: number;
|
|
33
|
+
};
|
|
34
|
+
/** Session max age in seconds (default: 86400 = 24h) */
|
|
35
|
+
sessionMaxAge: number;
|
|
36
|
+
/** Nonce expiry in seconds (default: 60) */
|
|
37
|
+
nonceExpiry: number;
|
|
38
|
+
/** Paths that skip TokenForge verification */
|
|
39
|
+
skipPaths?: string[];
|
|
40
|
+
/** Paths requiring elevated trust (>90) */
|
|
41
|
+
sensitiveOps?: string[];
|
|
42
|
+
/** Called on security events */
|
|
43
|
+
onSecurityEvent?: (event: SecurityEvent) => Promise<void>;
|
|
44
|
+
/** Email delivery callback for OTP step-up */
|
|
45
|
+
sendEmail?: (to: string, otp: string) => Promise<void>;
|
|
46
|
+
/** Get IP address from request (framework-specific) */
|
|
47
|
+
getIpAddress?: (req: unknown) => string;
|
|
48
|
+
/** Get country code from request (framework-specific) */
|
|
49
|
+
getCountryCode?: (req: unknown) => string;
|
|
50
|
+
/** Get user agent from request (framework-specific) */
|
|
51
|
+
getUserAgent?: (req: unknown) => string;
|
|
52
|
+
/** Retrieve stored WebAuthn public key (SPKI) for passkey verification */
|
|
53
|
+
getPasskeyPublicKey?: (userId: string, credentialId: string) => Promise<ArrayBuffer | null>;
|
|
54
|
+
/** Retrieve TOTP shared secret for a user */
|
|
55
|
+
getTotpSecret?: (userId: string) => Promise<string | null>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Storage reference — import from '@opensyber/tokenforge/server/storage'.
|
|
59
|
+
* This avoids a circular dep between types.ts and the storage interface.
|
|
60
|
+
*/
|
|
61
|
+
export interface TokenForgeStorageRef {
|
|
62
|
+
getSession(sessionId: string, deviceId: string): Promise<DeviceSession | null>;
|
|
63
|
+
createSession(session: DeviceSession): Promise<void>;
|
|
64
|
+
updateTrustScore(deviceId: string, trustScore: number): Promise<void>;
|
|
65
|
+
revokeSession(deviceId: string, reason: string): Promise<void>;
|
|
66
|
+
revokeUserSessions(userId: string): Promise<void>;
|
|
67
|
+
listUserSessions(userId: string, limit: number): Promise<DeviceSession[]>;
|
|
68
|
+
restoreTrust(deviceId: string, userId: string): Promise<void>;
|
|
69
|
+
hasNonce(nonce: string): Promise<boolean>;
|
|
70
|
+
storeNonce(nonce: string, ttlSeconds: number): Promise<void>;
|
|
71
|
+
logEvent(event: SecurityEvent & {
|
|
72
|
+
id: string;
|
|
73
|
+
}): Promise<void>;
|
|
74
|
+
listEvents(userId: string, limit: number, offset: number): Promise<(SecurityEvent & {
|
|
75
|
+
id: string;
|
|
76
|
+
created_at: string;
|
|
77
|
+
})[]>;
|
|
78
|
+
createChallenge(challenge: unknown): Promise<void>;
|
|
79
|
+
getChallenge(challengeId: string, userId: string): Promise<unknown>;
|
|
80
|
+
updateChallengeStatus(challengeId: string, status: string, completedAt?: string): Promise<void>;
|
|
81
|
+
countRecentChallenges(userId: string, windowMinutes: number): Promise<number>;
|
|
82
|
+
storeOtp(challengeId: string, code: string, ttlSeconds: number): Promise<void>;
|
|
83
|
+
getOtp(challengeId: string): Promise<string | null>;
|
|
84
|
+
deleteOtp(challengeId: string): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
/** A security event logged by the middleware */
|
|
87
|
+
export interface SecurityEvent {
|
|
88
|
+
sessionId: string;
|
|
89
|
+
userId: string;
|
|
90
|
+
eventType: SecurityEventType;
|
|
91
|
+
trustScoreBefore: number;
|
|
92
|
+
trustScoreAfter: number;
|
|
93
|
+
ipAddress: string;
|
|
94
|
+
countryCode: string;
|
|
95
|
+
userAgent: string;
|
|
96
|
+
metadata: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
/** All possible security event types */
|
|
99
|
+
export type SecurityEventType = 'DEVICE_BOUND' | 'SIGNATURE_INVALID' | 'IP_CHANGE' | 'GEO_ANOMALY' | 'TRUST_DROP' | 'TRUST_SCORE_CHANGE' | 'SESSION_REVOKED' | 'SESSION_EXPIRED' | 'STEP_UP_TRIGGERED' | 'STEP_UP_COMPLETED' | 'STEP_UP_FAILED' | 'NONCE_REPLAY' | 'DEVICE_MISMATCH' | 'DEVICE_NOT_FOUND' | 'TIMESTAMP_SKEW';
|
|
100
|
+
/** Signals used by the trust score engine */
|
|
101
|
+
export interface TrustSignals {
|
|
102
|
+
signatureValid: boolean;
|
|
103
|
+
ipAddress: string;
|
|
104
|
+
originalIp: string;
|
|
105
|
+
countryCode: string;
|
|
106
|
+
originalCountry: string;
|
|
107
|
+
userAgent: string;
|
|
108
|
+
originalFingerprint: string;
|
|
109
|
+
requestTimestamp: number;
|
|
110
|
+
sessionCreatedAt: number;
|
|
111
|
+
}
|
|
112
|
+
/** Breakdown of trust score components */
|
|
113
|
+
export interface ScoreBreakdown {
|
|
114
|
+
signatureScore: number;
|
|
115
|
+
ipScore: number;
|
|
116
|
+
geoScore: number;
|
|
117
|
+
fingerprintScore: number;
|
|
118
|
+
velocityScore: number;
|
|
119
|
+
timeScore: number;
|
|
120
|
+
nonceScore: number;
|
|
121
|
+
total: number;
|
|
122
|
+
reasons: string[];
|
|
123
|
+
}
|
|
124
|
+
/** Device session stored in D1 */
|
|
125
|
+
export interface DeviceSession {
|
|
126
|
+
id: string;
|
|
127
|
+
session_id: string;
|
|
128
|
+
user_id: string;
|
|
129
|
+
public_key: string;
|
|
130
|
+
device_fingerprint: string | null;
|
|
131
|
+
ip_address: string | null;
|
|
132
|
+
country_code: string | null;
|
|
133
|
+
trust_score: number;
|
|
134
|
+
bound_at: string;
|
|
135
|
+
last_verified_at: string;
|
|
136
|
+
expires_at: string;
|
|
137
|
+
revoked: number;
|
|
138
|
+
revoked_reason: string | null;
|
|
139
|
+
created_at: string;
|
|
140
|
+
}
|
|
141
|
+
/** Default header names */
|
|
142
|
+
export declare const TF_HEADERS: {
|
|
143
|
+
readonly SIGNATURE: "X-TF-Signature";
|
|
144
|
+
readonly NONCE: "X-TF-Nonce";
|
|
145
|
+
readonly TIMESTAMP: "X-TF-Timestamp";
|
|
146
|
+
readonly DEVICE_ID: "X-TF-Device-ID";
|
|
147
|
+
};
|
|
148
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAgKlC,2BAA2B;AAC3B,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,SAAS,EAAE,gBAAgB;IAC3B,KAAK,EAAE,YAAY;IACnB,SAAS,EAAE,gBAAgB;IAC3B,SAAS,EAAE,gBAAgB;CACnB,CAAC"}
|