@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.
Files changed (94) hide show
  1. package/README.md +137 -0
  2. package/dist/adapters/express.d.ts +48 -0
  3. package/dist/adapters/express.d.ts.map +1 -0
  4. package/dist/adapters/express.js +73 -0
  5. package/dist/adapters/express.js.map +1 -0
  6. package/dist/adapters/fastify.d.ts +44 -0
  7. package/dist/adapters/fastify.d.ts.map +1 -0
  8. package/dist/adapters/fastify.js +69 -0
  9. package/dist/adapters/fastify.js.map +1 -0
  10. package/dist/adapters/hono.d.ts +8 -0
  11. package/dist/adapters/hono.d.ts.map +1 -0
  12. package/dist/adapters/hono.js +8 -0
  13. package/dist/adapters/hono.js.map +1 -0
  14. package/dist/adapters/index.d.ts +5 -0
  15. package/dist/adapters/index.d.ts.map +1 -0
  16. package/dist/adapters/index.js +5 -0
  17. package/dist/adapters/index.js.map +1 -0
  18. package/dist/adapters/nextjs.d.ts +51 -0
  19. package/dist/adapters/nextjs.d.ts.map +1 -0
  20. package/dist/adapters/nextjs.js +101 -0
  21. package/dist/adapters/nextjs.js.map +1 -0
  22. package/dist/client/binding.d.ts +14 -0
  23. package/dist/client/binding.d.ts.map +1 -0
  24. package/dist/client/binding.js +49 -0
  25. package/dist/client/binding.js.map +1 -0
  26. package/dist/client/crypto.d.ts +16 -0
  27. package/dist/client/crypto.d.ts.map +1 -0
  28. package/dist/client/crypto.js +22 -0
  29. package/dist/client/crypto.js.map +1 -0
  30. package/dist/client/index.d.ts +38 -0
  31. package/dist/client/index.d.ts.map +1 -0
  32. package/dist/client/index.js +108 -0
  33. package/dist/client/index.js.map +1 -0
  34. package/dist/client/interceptor.d.ts +13 -0
  35. package/dist/client/interceptor.d.ts.map +1 -0
  36. package/dist/client/interceptor.js +85 -0
  37. package/dist/client/interceptor.js.map +1 -0
  38. package/dist/client/signer.d.ts +15 -0
  39. package/dist/client/signer.d.ts.map +1 -0
  40. package/dist/client/signer.js +31 -0
  41. package/dist/client/signer.js.map +1 -0
  42. package/dist/client/storage.d.ts +17 -0
  43. package/dist/client/storage.d.ts.map +1 -0
  44. package/dist/client/storage.js +58 -0
  45. package/dist/client/storage.js.map +1 -0
  46. package/dist/react/index.d.ts +2 -0
  47. package/dist/react/index.d.ts.map +1 -0
  48. package/dist/react/index.js +2 -0
  49. package/dist/react/index.js.map +1 -0
  50. package/dist/react/provider.d.ts +30 -0
  51. package/dist/react/provider.d.ts.map +1 -0
  52. package/dist/react/provider.js +59 -0
  53. package/dist/react/provider.js.map +1 -0
  54. package/dist/server/binding.d.ts +19 -0
  55. package/dist/server/binding.d.ts.map +1 -0
  56. package/dist/server/binding.js +87 -0
  57. package/dist/server/binding.js.map +1 -0
  58. package/dist/server/index.d.ts +7 -0
  59. package/dist/server/index.d.ts.map +1 -0
  60. package/dist/server/index.js +6 -0
  61. package/dist/server/index.js.map +1 -0
  62. package/dist/server/middleware.d.ts +30 -0
  63. package/dist/server/middleware.d.ts.map +1 -0
  64. package/dist/server/middleware.js +107 -0
  65. package/dist/server/middleware.js.map +1 -0
  66. package/dist/server/step-up.d.ts +19 -0
  67. package/dist/server/step-up.d.ts.map +1 -0
  68. package/dist/server/step-up.js +139 -0
  69. package/dist/server/step-up.js.map +1 -0
  70. package/dist/server/storage/index.d.ts +3 -0
  71. package/dist/server/storage/index.d.ts.map +1 -0
  72. package/dist/server/storage/index.js +2 -0
  73. package/dist/server/storage/index.js.map +1 -0
  74. package/dist/server/storage/interface.d.ts +61 -0
  75. package/dist/server/storage/interface.d.ts.map +1 -0
  76. package/dist/server/storage/interface.js +2 -0
  77. package/dist/server/storage/interface.js.map +1 -0
  78. package/dist/server/storage/memory.d.ts +40 -0
  79. package/dist/server/storage/memory.d.ts.map +1 -0
  80. package/dist/server/storage/memory.js +107 -0
  81. package/dist/server/storage/memory.js.map +1 -0
  82. package/dist/server/totp.d.ts +53 -0
  83. package/dist/server/totp.d.ts.map +1 -0
  84. package/dist/server/totp.js +114 -0
  85. package/dist/server/totp.js.map +1 -0
  86. package/dist/shared/types-internal.d.ts +66 -0
  87. package/dist/shared/types-internal.d.ts.map +1 -0
  88. package/dist/shared/types-internal.js +4 -0
  89. package/dist/shared/types-internal.js.map +1 -0
  90. package/dist/shared/types.d.ts +148 -0
  91. package/dist/shared/types.d.ts.map +1 -0
  92. package/dist/shared/types.js +9 -0
  93. package/dist/shared/types.js.map +1 -0
  94. package/package.json +118 -0
@@ -0,0 +1,49 @@
1
+ import { generateDeviceKeyPair, exportPublicKey } from './crypto.js';
2
+ import { storeDeviceKey, getDeviceKey } from './storage.js';
3
+ /**
4
+ * Complete device binding flow — called once after authentication.
5
+ *
6
+ * 1. Check if we already have a valid key for this session
7
+ * 2. If not, generate new keypair
8
+ * 3. Send public key to server to bind to session
9
+ * 4. Server stores public key and returns device ID
10
+ * 5. Store keypair + device ID locally
11
+ */
12
+ export async function bindDevice(apiBase, sessionId) {
13
+ const existing = await getDeviceKey();
14
+ if (existing && existing.sessionId === sessionId) {
15
+ return { deviceId: existing.deviceId, keyPair: existing.keyPair };
16
+ }
17
+ const keyPair = await generateDeviceKeyPair();
18
+ const publicKeyJwk = await exportPublicKey(keyPair);
19
+ const metadata = {
20
+ userAgent: navigator.userAgent,
21
+ language: navigator.language,
22
+ platform: navigator.platform,
23
+ screenResolution: `${screen.width}x${screen.height}`,
24
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
25
+ colorDepth: screen.colorDepth,
26
+ };
27
+ const response = await fetch(`${apiBase}/api/tf/bind`, {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ credentials: 'include',
31
+ body: JSON.stringify({
32
+ publicKey: publicKeyJwk,
33
+ sessionId,
34
+ metadata,
35
+ }),
36
+ });
37
+ if (!response.ok) {
38
+ throw new Error(`TokenForge binding failed: ${response.status}`);
39
+ }
40
+ const { deviceId } = (await response.json());
41
+ await storeDeviceKey({
42
+ deviceId,
43
+ keyPair,
44
+ createdAt: Date.now(),
45
+ sessionId,
46
+ });
47
+ return { deviceId, keyPair };
48
+ }
49
+ //# sourceMappingURL=binding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.js","sourceRoot":"","sources":["../../src/client/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5D;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAAe,EACf,SAAiB;IAEjB,MAAM,QAAQ,GAAG,MAAM,YAAY,EAAE,CAAC;IACtC,IAAI,QAAQ,IAAI,QAAQ,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QACjD,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC;IACpE,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,qBAAqB,EAAE,CAAC;IAC9C,MAAM,YAAY,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAEpD,MAAM,QAAQ,GAAmB;QAC/B,SAAS,EAAE,SAAS,CAAC,SAAS;QAC9B,QAAQ,EAAE,SAAS,CAAC,QAAQ;QAC5B,QAAQ,EAAE,SAAS,CAAC,QAAQ;QAC5B,gBAAgB,EAAE,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE;QACpD,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,eAAe,EAAE,CAAC,QAAQ;QAC1D,UAAU,EAAE,MAAM,CAAC,UAAU;KAC9B,CAAC;IAEF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,cAAc,EAAE;QACrD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,WAAW,EAAE,SAAS;QACtB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,SAAS,EAAE,YAAY;YACvB,SAAS;YACT,QAAQ;SACT,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,8BAA8B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAyB,CAAC;IAErE,MAAM,cAAc,CAAC;QACnB,QAAQ;QACR,OAAO;QACP,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,SAAS;KACV,CAAC,CAAC;IAEH,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Client-side cryptographic key generation and export.
3
+ * Uses Web Crypto API with NON-EXTRACTABLE private keys.
4
+ */
5
+ /**
6
+ * Generate a new ECDSA P-256 keypair.
7
+ * The private key is NON-EXTRACTABLE — it can never be read as raw bytes,
8
+ * only used for signing operations within the browser's crypto engine.
9
+ */
10
+ export declare function generateDeviceKeyPair(): Promise<CryptoKeyPair>;
11
+ /**
12
+ * Export the public key as JWK for sending to server during binding.
13
+ * Only the PUBLIC key is exportable. Private key stays locked.
14
+ */
15
+ export declare function exportPublicKey(keyPair: CryptoKeyPair): Promise<JsonWebKey>;
16
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/client/crypto.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,aAAa,CAAC,CAMpE;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,UAAU,CAAC,CAErB"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Client-side cryptographic key generation and export.
3
+ * Uses Web Crypto API with NON-EXTRACTABLE private keys.
4
+ */
5
+ const KEY_ALGORITHM = { name: 'ECDSA', namedCurve: 'P-256' };
6
+ /**
7
+ * Generate a new ECDSA P-256 keypair.
8
+ * The private key is NON-EXTRACTABLE — it can never be read as raw bytes,
9
+ * only used for signing operations within the browser's crypto engine.
10
+ */
11
+ export async function generateDeviceKeyPair() {
12
+ return await crypto.subtle.generateKey(KEY_ALGORITHM, false, // NON-EXTRACTABLE
13
+ ['sign', 'verify']);
14
+ }
15
+ /**
16
+ * Export the public key as JWK for sending to server during binding.
17
+ * Only the PUBLIC key is exportable. Private key stays locked.
18
+ */
19
+ export async function exportPublicKey(keyPair) {
20
+ return await crypto.subtle.exportKey('jwk', keyPair.publicKey);
21
+ }
22
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/client/crypto.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,aAAa,GAAmB,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;AAE7E;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,OAAO,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CACpC,aAAa,EACb,KAAK,EAAE,kBAAkB;IACzB,CAAC,MAAM,EAAE,QAAQ,CAAC,CACnB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAAsB;IAEtB,OAAO,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;AACjE,CAAC"}
@@ -0,0 +1,38 @@
1
+ import type { TokenForgeConfig } from '../shared/types.js';
2
+ /**
3
+ * TokenForge Client SDK — main entry point.
4
+ *
5
+ * Usage:
6
+ * const tf = createTokenForge({ apiBase: '/api', getSessionId: () => sid });
7
+ * await tf.init();
8
+ */
9
+ export declare class TokenForge {
10
+ private config;
11
+ private deviceId;
12
+ private keyPair;
13
+ private bound;
14
+ private cleanupInterceptor;
15
+ constructor(config: TokenForgeConfig);
16
+ /**
17
+ * Initialize — call after user authenticates.
18
+ * Generates keypair if needed, binds to session, installs interceptor.
19
+ */
20
+ init(): Promise<void>;
21
+ /**
22
+ * Manually sign a request (if autoIntercept is false).
23
+ */
24
+ signRequest(request: Request): Promise<Request>;
25
+ /** @returns The current device ID, or null if not yet bound. */
26
+ getDeviceId(): string | null;
27
+ /** @returns Whether the device has been successfully bound to the session. */
28
+ isBound(): boolean;
29
+ /** Clear stored keys and unbind the device. Removes the fetch interceptor. */
30
+ clearKeys(): Promise<void>;
31
+ /** Clear existing binding and re-initialize (generate new keypair + bind). */
32
+ rebind(): Promise<void>;
33
+ }
34
+ /**
35
+ * Factory function to create a TokenForge instance.
36
+ */
37
+ export declare function createTokenForge(config: TokenForgeConfig): TokenForge;
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAK3D;;;;;;GAMG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,kBAAkB,CAA6B;gBAE3C,MAAM,EAAE,gBAAgB;IAIpC;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAuC3B;;OAEG;IACG,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IA+BrD,gEAAgE;IAChE,WAAW,IAAI,MAAM,GAAG,IAAI;IAI5B,8EAA8E;IAC9E,OAAO,IAAI,OAAO;IAIlB,8EAA8E;IACxE,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAShC,8EAA8E;IACxE,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAI9B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CAErE"}
@@ -0,0 +1,108 @@
1
+ import { bindDevice } from './binding.js';
2
+ import { clearDeviceKeys } from './storage.js';
3
+ import { installFetchInterceptor } from './interceptor.js';
4
+ /**
5
+ * TokenForge Client SDK — main entry point.
6
+ *
7
+ * Usage:
8
+ * const tf = createTokenForge({ apiBase: '/api', getSessionId: () => sid });
9
+ * await tf.init();
10
+ */
11
+ export class TokenForge {
12
+ config;
13
+ deviceId = null;
14
+ keyPair = null;
15
+ bound = false;
16
+ cleanupInterceptor = null;
17
+ constructor(config) {
18
+ this.config = config;
19
+ }
20
+ /**
21
+ * Initialize — call after user authenticates.
22
+ * Generates keypair if needed, binds to session, installs interceptor.
23
+ */
24
+ async init() {
25
+ const sessionId = this.config.getSessionId();
26
+ if (!sessionId)
27
+ return;
28
+ // Check if Web Crypto is available
29
+ if (!crypto?.subtle) {
30
+ console.warn('TokenForge: Web Crypto API not available, running in degraded mode');
31
+ return;
32
+ }
33
+ try {
34
+ const result = await bindDevice(this.config.apiBase, sessionId);
35
+ this.deviceId = result.deviceId;
36
+ this.keyPair = result.keyPair;
37
+ this.bound = true;
38
+ this.config.onDeviceBound?.(this.deviceId);
39
+ if (this.config.autoIntercept !== false) {
40
+ this.cleanupInterceptor = installFetchInterceptor(this.config, async () => {
41
+ if (!this.keyPair || !this.deviceId)
42
+ return null;
43
+ const sid = this.config.getSessionId();
44
+ if (!sid)
45
+ return null;
46
+ return {
47
+ privateKey: this.keyPair.privateKey,
48
+ sessionId: sid,
49
+ deviceId: this.deviceId,
50
+ };
51
+ });
52
+ }
53
+ }
54
+ catch (err) {
55
+ console.error('TokenForge: Binding failed', err);
56
+ // Graceful degradation — app continues without device binding
57
+ }
58
+ }
59
+ /**
60
+ * Manually sign a request (if autoIntercept is false).
61
+ */
62
+ async signRequest(request) {
63
+ if (!this.keyPair || !this.deviceId)
64
+ return request;
65
+ const { signChallenge, generateNonce } = await import('./signer.js');
66
+ const sessionId = this.config.getSessionId();
67
+ if (!sessionId)
68
+ return request;
69
+ const nonce = generateNonce();
70
+ const timestamp = Math.floor(Date.now() / 1000);
71
+ const signature = await signChallenge(this.keyPair.privateKey, sessionId, nonce, timestamp);
72
+ const headers = new Headers(request.headers);
73
+ headers.set(this.config.headers?.signature || 'X-TF-Signature', signature);
74
+ headers.set(this.config.headers?.nonce || 'X-TF-Nonce', nonce);
75
+ headers.set(this.config.headers?.timestamp || 'X-TF-Timestamp', timestamp.toString());
76
+ headers.set(this.config.headers?.deviceId || 'X-TF-Device-ID', this.deviceId);
77
+ return new Request(request, { headers });
78
+ }
79
+ /** @returns The current device ID, or null if not yet bound. */
80
+ getDeviceId() {
81
+ return this.deviceId;
82
+ }
83
+ /** @returns Whether the device has been successfully bound to the session. */
84
+ isBound() {
85
+ return this.bound;
86
+ }
87
+ /** Clear stored keys and unbind the device. Removes the fetch interceptor. */
88
+ async clearKeys() {
89
+ this.cleanupInterceptor?.();
90
+ this.cleanupInterceptor = null;
91
+ this.keyPair = null;
92
+ this.deviceId = null;
93
+ this.bound = false;
94
+ await clearDeviceKeys();
95
+ }
96
+ /** Clear existing binding and re-initialize (generate new keypair + bind). */
97
+ async rebind() {
98
+ await this.clearKeys();
99
+ await this.init();
100
+ }
101
+ }
102
+ /**
103
+ * Factory function to create a TokenForge instance.
104
+ */
105
+ export function createTokenForge(config) {
106
+ return new TokenForge(config);
107
+ }
108
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { TokenForgeConfig } from '../shared/types.js';
2
+ /**
3
+ * Install a fetch interceptor that automatically signs all requests
4
+ * to the protected API with TokenForge headers.
5
+ *
6
+ * Application code continues to use fetch() normally.
7
+ */
8
+ export declare function installFetchInterceptor(config: TokenForgeConfig, getSigningMaterial: () => Promise<{
9
+ privateKey: CryptoKey;
10
+ sessionId: string;
11
+ deviceId: string;
12
+ } | null>): () => void;
13
+ //# sourceMappingURL=interceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../../src/client/interceptor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,gBAAgB,EACxB,kBAAkB,EAAE,MAAM,OAAO,CAAC;IAChC,UAAU,EAAE,SAAS,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,IAAI,CAAC,GACR,MAAM,IAAI,CAqFZ"}
@@ -0,0 +1,85 @@
1
+ import { signChallenge, generateNonce } from './signer.js';
2
+ /**
3
+ * Install a fetch interceptor that automatically signs all requests
4
+ * to the protected API with TokenForge headers.
5
+ *
6
+ * Application code continues to use fetch() normally.
7
+ */
8
+ export function installFetchInterceptor(config, getSigningMaterial) {
9
+ const originalFetch = window.fetch.bind(window);
10
+ window.fetch = async function (input, init) {
11
+ const url = typeof input === 'string'
12
+ ? input
13
+ : input instanceof URL
14
+ ? input.href
15
+ : input.url;
16
+ if (!shouldIntercept(url, config)) {
17
+ return originalFetch(input, init);
18
+ }
19
+ const material = await getSigningMaterial();
20
+ if (!material) {
21
+ return originalFetch(input, init);
22
+ }
23
+ const nonce = generateNonce();
24
+ const timestamp = Math.floor(Date.now() / 1000);
25
+ const signature = await signChallenge(material.privateKey, material.sessionId, nonce, timestamp);
26
+ const headers = new Headers(init?.headers);
27
+ headers.set(config.headers?.signature || 'X-TF-Signature', signature);
28
+ headers.set(config.headers?.nonce || 'X-TF-Nonce', nonce);
29
+ headers.set(config.headers?.timestamp || 'X-TF-Timestamp', timestamp.toString());
30
+ headers.set(config.headers?.deviceId || 'X-TF-Device-ID', material.deviceId);
31
+ const response = await originalFetch(input, { ...init, headers });
32
+ if (response.status === 403) {
33
+ const body = await response
34
+ .clone()
35
+ .json()
36
+ .catch(() => null);
37
+ if (body &&
38
+ typeof body === 'object' &&
39
+ 'action' in body &&
40
+ body.action === 'step_up_required') {
41
+ config.onStepUpRequired?.(('reason' in body ? String(body.reason) : '') || 'trust_score_low');
42
+ }
43
+ }
44
+ if (response.status === 401) {
45
+ const body = await response
46
+ .clone()
47
+ .json()
48
+ .catch(() => null);
49
+ if (body &&
50
+ typeof body === 'object' &&
51
+ 'action' in body &&
52
+ body.action === 'session_revoked') {
53
+ config.onSessionRevoked?.();
54
+ }
55
+ }
56
+ return response;
57
+ };
58
+ // Return cleanup function
59
+ return () => {
60
+ window.fetch = originalFetch;
61
+ };
62
+ }
63
+ function shouldIntercept(url, config) {
64
+ // Only intercept requests to the API base
65
+ if (!url.startsWith(config.apiBase) && !url.startsWith('/')) {
66
+ return false;
67
+ }
68
+ // Skip excluded paths
69
+ if (config.skipPaths) {
70
+ const path = url.startsWith('http')
71
+ ? new URL(url).pathname
72
+ : url;
73
+ for (const skip of config.skipPaths) {
74
+ if (skip.endsWith('*')) {
75
+ if (path.startsWith(skip.slice(0, -1)))
76
+ return false;
77
+ }
78
+ else if (path === skip) {
79
+ return false;
80
+ }
81
+ }
82
+ }
83
+ return true;
84
+ }
85
+ //# sourceMappingURL=interceptor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interceptor.js","sourceRoot":"","sources":["../../src/client/interceptor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Challenge-response signing protocol.
3
+ * Signs: `${sessionId}:${nonce}:${timestamp}` with the device-bound private key.
4
+ */
5
+ /**
6
+ * Sign a challenge payload with the device-bound private key.
7
+ */
8
+ export declare function signChallenge(privateKey: CryptoKey, sessionId: string, nonce: string, timestamp: number): Promise<string>;
9
+ /**
10
+ * Generate a client-side nonce.
11
+ * Server will verify this hasn't been used before.
12
+ */
13
+ export declare function generateNonce(): string;
14
+ export declare function arrayBufferToBase64Url(buffer: ArrayBuffer): string;
15
+ //# sourceMappingURL=signer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../../src/client/signer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,SAAS,EACrB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAWjB;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAItC;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAOlE"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Challenge-response signing protocol.
3
+ * Signs: `${sessionId}:${nonce}:${timestamp}` with the device-bound private key.
4
+ */
5
+ /**
6
+ * Sign a challenge payload with the device-bound private key.
7
+ */
8
+ export async function signChallenge(privateKey, sessionId, nonce, timestamp) {
9
+ const payload = `${sessionId}:${nonce}:${timestamp}`;
10
+ const encoded = new TextEncoder().encode(payload);
11
+ const signature = await crypto.subtle.sign({ name: 'ECDSA', hash: { name: 'SHA-256' } }, privateKey, encoded);
12
+ return arrayBufferToBase64Url(signature);
13
+ }
14
+ /**
15
+ * Generate a client-side nonce.
16
+ * Server will verify this hasn't been used before.
17
+ */
18
+ export function generateNonce() {
19
+ const bytes = new Uint8Array(16);
20
+ crypto.getRandomValues(bytes);
21
+ return arrayBufferToBase64Url(bytes.buffer);
22
+ }
23
+ export function arrayBufferToBase64Url(buffer) {
24
+ const bytes = new Uint8Array(buffer);
25
+ let binary = '';
26
+ for (const byte of bytes) {
27
+ binary += String.fromCharCode(byte);
28
+ }
29
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
30
+ }
31
+ //# sourceMappingURL=signer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signer.js","sourceRoot":"","sources":["../../src/client/signer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,UAAqB,EACrB,SAAiB,EACjB,KAAa,EACb,SAAiB;IAEjB,MAAM,OAAO,GAAG,GAAG,SAAS,IAAI,KAAK,IAAI,SAAS,EAAE,CAAC;IACrD,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAElD,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CACxC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAC5C,UAAU,EACV,OAAO,CACR,CAAC;IAEF,OAAO,sBAAsB,CAAC,SAAS,CAAC,CAAC;AAC3C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,sBAAsB,CAAC,KAAK,CAAC,MAAqB,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAmB;IACxD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IACrC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAChF,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { StoredDevice } from '../shared/types-internal.js';
2
+ /**
3
+ * Store keypair in IndexedDB.
4
+ * IndexedDB can store CryptoKey objects directly (structured clone).
5
+ * The non-extractable property is preserved across storage.
6
+ */
7
+ export declare function storeDeviceKey(device: StoredDevice): Promise<void>;
8
+ /**
9
+ * Retrieve stored keypair from IndexedDB.
10
+ * Returns the CryptoKey handle — still non-extractable.
11
+ */
12
+ export declare function getDeviceKey(): Promise<StoredDevice | null>;
13
+ /**
14
+ * Clear all stored keys (on logout or re-bind).
15
+ */
16
+ export declare function clearDeviceKeys(): Promise<void>;
17
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/client/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAKhE;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAUxE;AAED;;;GAGG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAUjE;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAUrD"}
@@ -0,0 +1,58 @@
1
+ const DB_NAME = 'tokenforge';
2
+ const STORE_NAME = 'device_keys';
3
+ /**
4
+ * Store keypair in IndexedDB.
5
+ * IndexedDB can store CryptoKey objects directly (structured clone).
6
+ * The non-extractable property is preserved across storage.
7
+ */
8
+ export async function storeDeviceKey(device) {
9
+ const db = await openDB();
10
+ return new Promise((resolve, reject) => {
11
+ const tx = db.transaction(STORE_NAME, 'readwrite');
12
+ const store = tx.objectStore(STORE_NAME);
13
+ const req = store.put(device, 'current');
14
+ req.onsuccess = () => resolve();
15
+ req.onerror = () => reject(req.error);
16
+ tx.oncomplete = () => db.close();
17
+ });
18
+ }
19
+ /**
20
+ * Retrieve stored keypair from IndexedDB.
21
+ * Returns the CryptoKey handle — still non-extractable.
22
+ */
23
+ export async function getDeviceKey() {
24
+ const db = await openDB();
25
+ return new Promise((resolve, reject) => {
26
+ const tx = db.transaction(STORE_NAME, 'readonly');
27
+ const store = tx.objectStore(STORE_NAME);
28
+ const req = store.get('current');
29
+ req.onsuccess = () => resolve(req.result || null);
30
+ req.onerror = () => reject(req.error);
31
+ tx.oncomplete = () => db.close();
32
+ });
33
+ }
34
+ /**
35
+ * Clear all stored keys (on logout or re-bind).
36
+ */
37
+ export async function clearDeviceKeys() {
38
+ const db = await openDB();
39
+ return new Promise((resolve, reject) => {
40
+ const tx = db.transaction(STORE_NAME, 'readwrite');
41
+ const store = tx.objectStore(STORE_NAME);
42
+ const req = store.clear();
43
+ req.onsuccess = () => resolve();
44
+ req.onerror = () => reject(req.error);
45
+ tx.oncomplete = () => db.close();
46
+ });
47
+ }
48
+ function openDB() {
49
+ return new Promise((resolve, reject) => {
50
+ const request = indexedDB.open(DB_NAME, 1);
51
+ request.onupgradeneeded = () => {
52
+ request.result.createObjectStore(STORE_NAME);
53
+ };
54
+ request.onsuccess = () => resolve(request.result);
55
+ request.onerror = () => reject(request.error);
56
+ });
57
+ }
58
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/client/storage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { TokenForgeProvider, useTokenForge } from './provider.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { TokenForgeProvider, useTokenForge } from './provider.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { type ReactNode } from 'react';
2
+ interface TokenForgeContextValue {
3
+ isReady: boolean;
4
+ isBound: boolean;
5
+ deviceId: string | null;
6
+ trustScore: number | null;
7
+ }
8
+ interface TokenForgeProviderProps {
9
+ children: ReactNode;
10
+ /** The current session ID from your auth provider */
11
+ sessionId: string | null;
12
+ /** Whether the user is signed in */
13
+ isSignedIn: boolean;
14
+ /** Base URL for the TokenForge-protected API */
15
+ apiBase: string;
16
+ }
17
+ /**
18
+ * React context provider for TokenForge device binding.
19
+ * Initializes the client SDK when the user is signed in and
20
+ * exposes binding state to descendant components via `useTokenForge`.
21
+ * @param props - Provider props (children, sessionId, isSignedIn, apiBase).
22
+ */
23
+ export declare function TokenForgeProvider({ children, sessionId, isSignedIn, apiBase, }: TokenForgeProviderProps): import("react/jsx-runtime").JSX.Element;
24
+ /**
25
+ * Access the TokenForge device-binding context.
26
+ * @returns Current binding state (isReady, isBound, deviceId, trustScore).
27
+ */
28
+ export declare function useTokenForge(): TokenForgeContextValue;
29
+ export {};
30
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/react/provider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,UAAU,sBAAsB;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AASD,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,SAAS,CAAC;IACpB,qDAAqD;IACrD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,oCAAoC;IACpC,UAAU,EAAE,OAAO,CAAC;IACpB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,SAAS,EACT,UAAU,EACV,OAAO,GACR,EAAE,uBAAuB,2CA8CzB;AAED;;;GAGG;AACH,wBAAgB,aAAa,2BAE5B"}
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useEffect, useState, } from 'react';
4
+ import { createTokenForge } from '../client/index.js';
5
+ const TokenForgeContext = createContext({
6
+ isReady: false,
7
+ isBound: false,
8
+ deviceId: null,
9
+ trustScore: null,
10
+ });
11
+ /**
12
+ * React context provider for TokenForge device binding.
13
+ * Initializes the client SDK when the user is signed in and
14
+ * exposes binding state to descendant components via `useTokenForge`.
15
+ * @param props - Provider props (children, sessionId, isSignedIn, apiBase).
16
+ */
17
+ export function TokenForgeProvider({ children, sessionId, isSignedIn, apiBase, }) {
18
+ const [state, setState] = useState({
19
+ isReady: false,
20
+ isBound: false,
21
+ deviceId: null,
22
+ trustScore: null,
23
+ });
24
+ useEffect(() => {
25
+ if (!isSignedIn || !sessionId)
26
+ return;
27
+ let tf = null;
28
+ const setup = async () => {
29
+ tf = createTokenForge({
30
+ apiBase,
31
+ getSessionId: () => sessionId,
32
+ onStepUpRequired: (reason) => {
33
+ window.dispatchEvent(new CustomEvent('tf:step-up', { detail: reason }));
34
+ },
35
+ onSessionRevoked: () => {
36
+ window.location.href = '/sign-in?reason=session_revoked';
37
+ },
38
+ onDeviceBound: (deviceId) => {
39
+ setState((s) => ({ ...s, isBound: true, deviceId }));
40
+ },
41
+ });
42
+ await tf.init();
43
+ setState((s) => ({ ...s, isReady: true }));
44
+ };
45
+ setup().catch(console.error);
46
+ return () => {
47
+ tf?.clearKeys().catch(console.error);
48
+ };
49
+ }, [sessionId, isSignedIn, apiBase]);
50
+ return (_jsx(TokenForgeContext.Provider, { value: state, children: children }));
51
+ }
52
+ /**
53
+ * Access the TokenForge device-binding context.
54
+ * @returns Current binding state (isReady, isBound, deviceId, trustScore).
55
+ */
56
+ export function useTokenForge() {
57
+ return useContext(TokenForgeContext);
58
+ }
59
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/react/provider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EACL,aAAa,EACb,UAAU,EACV,SAAS,EACT,QAAQ,GAET,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,gBAAgB,EAAmB,MAAM,oBAAoB,CAAC;AASvE,MAAM,iBAAiB,GAAG,aAAa,CAAyB;IAC9D,OAAO,EAAE,KAAK;IACd,OAAO,EAAE,KAAK;IACd,QAAQ,EAAE,IAAI;IACd,UAAU,EAAE,IAAI;CACjB,CAAC,CAAC;AAYH;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,EACjC,QAAQ,EACR,SAAS,EACT,UAAU,EACV,OAAO,GACiB;IACxB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAyB;QACzD,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,KAAK;QACd,QAAQ,EAAE,IAAI;QACd,UAAU,EAAE,IAAI;KACjB,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS;YAAE,OAAO;QAEtC,IAAI,EAAE,GAAsB,IAAI,CAAC;QAEjC,MAAM,KAAK,GAAG,KAAK,IAAI,EAAE;YACvB,EAAE,GAAG,gBAAgB,CAAC;gBACpB,OAAO;gBACP,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS;gBAC7B,gBAAgB,EAAE,CAAC,MAAM,EAAE,EAAE;oBAC3B,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAClD,CAAC;gBACJ,CAAC;gBACD,gBAAgB,EAAE,GAAG,EAAE;oBACrB,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,iCAAiC,CAAC;gBAC3D,CAAC;gBACD,aAAa,EAAE,CAAC,QAAQ,EAAE,EAAE;oBAC1B,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;gBACvD,CAAC;aACF,CAAC,CAAC;YAEH,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC;YAChB,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC7C,CAAC,CAAC;QAEF,KAAK,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAE7B,OAAO,GAAG,EAAE;YACV,EAAE,EAAE,SAAS,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;IAErC,OAAO,CACL,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACrC,QAAQ,GACkB,CAC9B,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,CAAC,iBAAiB,CAAC,CAAC;AACvC,CAAC"}