@opensyber/tokenforge 0.1.2 → 1.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 (184) hide show
  1. package/LICENSE.md +32 -0
  2. package/README.md +329 -52
  3. package/dist/adapters/astro.d.ts +64 -0
  4. package/dist/adapters/astro.d.ts.map +1 -0
  5. package/dist/adapters/astro.js +104 -0
  6. package/dist/adapters/astro.js.map +1 -0
  7. package/dist/adapters/cloud-v2-bridge.d.ts +8 -0
  8. package/dist/adapters/cloud-v2-bridge.d.ts.map +1 -0
  9. package/dist/adapters/cloud-v2-bridge.js +24 -0
  10. package/dist/adapters/cloud-v2-bridge.js.map +1 -0
  11. package/dist/adapters/express.d.ts +30 -2
  12. package/dist/adapters/express.d.ts.map +1 -1
  13. package/dist/adapters/express.js +65 -0
  14. package/dist/adapters/express.js.map +1 -1
  15. package/dist/adapters/fastify.d.ts +32 -6
  16. package/dist/adapters/fastify.d.ts.map +1 -1
  17. package/dist/adapters/fastify.js +65 -0
  18. package/dist/adapters/fastify.js.map +1 -1
  19. package/dist/adapters/hono.d.ts +13 -1
  20. package/dist/adapters/hono.d.ts.map +1 -1
  21. package/dist/adapters/hono.js +14 -1
  22. package/dist/adapters/hono.js.map +1 -1
  23. package/dist/adapters/index.d.ts +5 -3
  24. package/dist/adapters/index.d.ts.map +1 -1
  25. package/dist/adapters/index.js +5 -3
  26. package/dist/adapters/index.js.map +1 -1
  27. package/dist/adapters/nextjs.d.ts +31 -0
  28. package/dist/adapters/nextjs.d.ts.map +1 -1
  29. package/dist/adapters/nextjs.js +83 -30
  30. package/dist/adapters/nextjs.js.map +1 -1
  31. package/dist/adapters/sveltekit.d.ts +66 -0
  32. package/dist/adapters/sveltekit.d.ts.map +1 -0
  33. package/dist/adapters/sveltekit.js +101 -0
  34. package/dist/adapters/sveltekit.js.map +1 -0
  35. package/dist/client/action-signer.d.ts +53 -0
  36. package/dist/client/action-signer.d.ts.map +1 -0
  37. package/dist/client/action-signer.js +48 -0
  38. package/dist/client/action-signer.js.map +1 -0
  39. package/dist/client/binding.d.ts +24 -7
  40. package/dist/client/binding.d.ts.map +1 -1
  41. package/dist/client/binding.js +17 -6
  42. package/dist/client/binding.js.map +1 -1
  43. package/dist/client/cloud-device.d.ts +54 -0
  44. package/dist/client/cloud-device.d.ts.map +1 -0
  45. package/dist/client/cloud-device.js +301 -0
  46. package/dist/client/cloud-device.js.map +1 -0
  47. package/dist/client/crypto.d.ts.map +1 -1
  48. package/dist/client/crypto.js.map +1 -1
  49. package/dist/client/index.d.ts +14 -0
  50. package/dist/client/index.d.ts.map +1 -1
  51. package/dist/client/index.js +30 -2
  52. package/dist/client/index.js.map +1 -1
  53. package/dist/client/interceptor.d.ts.map +1 -1
  54. package/dist/client/interceptor.js +27 -3
  55. package/dist/client/interceptor.js.map +1 -1
  56. package/dist/client/signer.d.ts +40 -3
  57. package/dist/client/signer.d.ts.map +1 -1
  58. package/dist/client/signer.js +56 -5
  59. package/dist/client/signer.js.map +1 -1
  60. package/dist/client/storage.d.ts.map +1 -1
  61. package/dist/client/storage.js.map +1 -1
  62. package/dist/client/webauthn.d.ts +45 -0
  63. package/dist/client/webauthn.d.ts.map +1 -0
  64. package/dist/client/webauthn.js +118 -0
  65. package/dist/client/webauthn.js.map +1 -0
  66. package/dist/protocol/v2.d.ts +23 -0
  67. package/dist/protocol/v2.d.ts.map +1 -0
  68. package/dist/protocol/v2.js +157 -0
  69. package/dist/protocol/v2.js.map +1 -0
  70. package/dist/react/index.d.ts.map +1 -1
  71. package/dist/react/index.js.map +1 -1
  72. package/dist/react/provider.d.ts.map +1 -1
  73. package/dist/react/provider.js.map +1 -1
  74. package/dist/server/action-verify.d.ts +56 -0
  75. package/dist/server/action-verify.d.ts.map +1 -0
  76. package/dist/server/action-verify.js +47 -0
  77. package/dist/server/action-verify.js.map +1 -0
  78. package/dist/server/activity-histogram.d.ts +47 -0
  79. package/dist/server/activity-histogram.d.ts.map +1 -0
  80. package/dist/server/activity-histogram.js +76 -0
  81. package/dist/server/activity-histogram.js.map +1 -0
  82. package/dist/server/aitm-heuristics.d.ts +57 -0
  83. package/dist/server/aitm-heuristics.d.ts.map +1 -0
  84. package/dist/server/aitm-heuristics.js +120 -0
  85. package/dist/server/aitm-heuristics.js.map +1 -0
  86. package/dist/server/bind-with-id-token.d.ts +55 -0
  87. package/dist/server/bind-with-id-token.d.ts.map +1 -0
  88. package/dist/server/bind-with-id-token.js +70 -0
  89. package/dist/server/bind-with-id-token.js.map +1 -0
  90. package/dist/server/bound-cookie.d.ts +39 -0
  91. package/dist/server/bound-cookie.d.ts.map +1 -0
  92. package/dist/server/bound-cookie.js +59 -0
  93. package/dist/server/bound-cookie.js.map +1 -0
  94. package/dist/server/cbor.d.ts +20 -0
  95. package/dist/server/cbor.d.ts.map +1 -0
  96. package/dist/server/cbor.js +110 -0
  97. package/dist/server/cbor.js.map +1 -0
  98. package/dist/server/cloud-bind.d.ts +42 -0
  99. package/dist/server/cloud-bind.d.ts.map +1 -0
  100. package/dist/server/cloud-bind.js +387 -0
  101. package/dist/server/cloud-bind.js.map +1 -0
  102. package/dist/server/cloud-middleware.d.ts +46 -0
  103. package/dist/server/cloud-middleware.d.ts.map +1 -0
  104. package/dist/server/cloud-middleware.js +344 -0
  105. package/dist/server/cloud-middleware.js.map +1 -0
  106. package/dist/server/cloud-verify.d.ts +30 -0
  107. package/dist/server/cloud-verify.d.ts.map +1 -0
  108. package/dist/server/cloud-verify.js +206 -0
  109. package/dist/server/cloud-verify.js.map +1 -0
  110. package/dist/server/dbsc-challenge.d.ts +51 -0
  111. package/dist/server/dbsc-challenge.d.ts.map +1 -0
  112. package/dist/server/dbsc-challenge.js +78 -0
  113. package/dist/server/dbsc-challenge.js.map +1 -0
  114. package/dist/server/dbsc-endpoint.d.ts +20 -0
  115. package/dist/server/dbsc-endpoint.d.ts.map +1 -0
  116. package/dist/server/dbsc-endpoint.js +85 -0
  117. package/dist/server/dbsc-endpoint.js.map +1 -0
  118. package/dist/server/dbsc-verify.d.ts +32 -0
  119. package/dist/server/dbsc-verify.d.ts.map +1 -0
  120. package/dist/server/dbsc-verify.js +65 -0
  121. package/dist/server/dbsc-verify.js.map +1 -0
  122. package/dist/server/identity-profile.d.ts +18 -0
  123. package/dist/server/identity-profile.d.ts.map +1 -0
  124. package/dist/server/identity-profile.js +113 -0
  125. package/dist/server/identity-profile.js.map +1 -0
  126. package/dist/server/index.d.ts +12 -0
  127. package/dist/server/index.d.ts.map +1 -1
  128. package/dist/server/index.js +7 -0
  129. package/dist/server/index.js.map +1 -1
  130. package/dist/server/jws-verify.d.ts +46 -0
  131. package/dist/server/jws-verify.d.ts.map +1 -0
  132. package/dist/server/jws-verify.js +64 -0
  133. package/dist/server/jws-verify.js.map +1 -0
  134. package/dist/server/middleware.d.ts +24 -0
  135. package/dist/server/middleware.d.ts.map +1 -1
  136. package/dist/server/middleware.js +87 -20
  137. package/dist/server/middleware.js.map +1 -1
  138. package/dist/server/oidc-verify.d.ts +71 -0
  139. package/dist/server/oidc-verify.d.ts.map +1 -0
  140. package/dist/server/oidc-verify.js +115 -0
  141. package/dist/server/oidc-verify.js.map +1 -0
  142. package/dist/server/policy.d.ts +47 -0
  143. package/dist/server/policy.d.ts.map +1 -0
  144. package/dist/server/policy.js +84 -0
  145. package/dist/server/policy.js.map +1 -0
  146. package/dist/server/step-up-policy.d.ts +36 -0
  147. package/dist/server/step-up-policy.d.ts.map +1 -0
  148. package/dist/server/step-up-policy.js +101 -0
  149. package/dist/server/step-up-policy.js.map +1 -0
  150. package/dist/server/storage/index.d.ts +3 -0
  151. package/dist/server/storage/index.d.ts.map +1 -0
  152. package/dist/server/storage/index.js +2 -0
  153. package/dist/server/storage/index.js.map +1 -0
  154. package/dist/server/storage/interface.d.ts +73 -0
  155. package/dist/server/storage/interface.d.ts.map +1 -0
  156. package/dist/server/storage/interface.js +2 -0
  157. package/dist/server/storage/interface.js.map +1 -0
  158. package/dist/server/storage/memory.d.ts +50 -0
  159. package/dist/server/storage/memory.d.ts.map +1 -0
  160. package/dist/server/storage/memory.js +121 -0
  161. package/dist/server/storage/memory.js.map +1 -0
  162. package/dist/server/webauthn-verify.d.ts +41 -0
  163. package/dist/server/webauthn-verify.d.ts.map +1 -0
  164. package/dist/server/webauthn-verify.js +175 -0
  165. package/dist/server/webauthn-verify.js.map +1 -0
  166. package/dist/server/webhooks.d.ts +42 -0
  167. package/dist/server/webhooks.d.ts.map +1 -0
  168. package/dist/server/webhooks.js +77 -0
  169. package/dist/server/webhooks.js.map +1 -0
  170. package/dist/shared/action-hash.d.ts +23 -0
  171. package/dist/shared/action-hash.d.ts.map +1 -0
  172. package/dist/shared/action-hash.js +35 -0
  173. package/dist/shared/action-hash.js.map +1 -0
  174. package/dist/shared/identity.d.ts +45 -0
  175. package/dist/shared/identity.d.ts.map +1 -0
  176. package/dist/shared/identity.js +176 -0
  177. package/dist/shared/identity.js.map +1 -0
  178. package/dist/shared/types-internal.d.ts.map +1 -1
  179. package/dist/shared/types-internal.js.map +1 -1
  180. package/dist/shared/types.d.ts +49 -2
  181. package/dist/shared/types.d.ts.map +1 -1
  182. package/dist/shared/types.js +1 -0
  183. package/dist/shared/types.js.map +1 -1
  184. package/package.json +31 -33
package/LICENSE.md ADDED
@@ -0,0 +1,32 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenSyber
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ ## Note on the hosted verification service
26
+
27
+ The TokenForge SDK code in this repository is MIT-licensed. The TokenForge
28
+ verification service hosted at `tokenforge.opensyber.cloud` requires a
29
+ TokenForge API key and is governed by the OpenSyber Terms of Service at
30
+ https://tokenforge.opensyber.cloud/terms. You are free to self-host the
31
+ server adapters using your own database / KV storage backend (see
32
+ `@opensyber/tokenforge/server/storage`) under MIT terms.
package/README.md CHANGED
@@ -1,108 +1,340 @@
1
1
  # @opensyber/tokenforge
2
2
 
3
- Device-bound session security. Add one script tag — every request is cryptographically signed with a device key that never leaves the browser. A stolen cookie without the device key is useless.
3
+ [![npm](https://img.shields.io/npm/v/@opensyber/tokenforge.svg)](https://www.npmjs.com/package/@opensyber/tokenforge)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE.md)
5
+
6
+ ## TokenForge Cloud v2 integration kit
7
+
8
+ The `1.1.0` Cloud path keeps the TokenForge bearer credential on the
9
+ application server. The browser creates a non-extractable P-256 key, binds only
10
+ through an authenticated application endpoint, and signs the exact v2 request
11
+ contract.
12
+
13
+ ```ts
14
+ import { TokenForgeDevice } from '@opensyber/tokenforge/client';
15
+
16
+ const device = await TokenForgeDevice.open({
17
+ bindEndpoint: '/api/v1/auth/device/bind',
18
+ fetch: authenticatedFetch,
19
+ });
20
+ await device.bindAuthenticatedSession();
21
+ export const secureFetch = device.wrapFetch(authenticatedFetch);
22
+ ```
23
+
24
+ The server adapter is responsible for session resolution, the server-only
25
+ credential, local/cloud decision composition, circuit breaking, and telemetry.
26
+ Set `TOKENFORGE_CLOUD_MODE` to `off`, `monitor`, or `enforce`; default/unknown
27
+ values are `off`. In `monitor`, a locally valid request stays available while
28
+ cloud failures and would-block outcomes are reported. In `enforce`, definitive
29
+ cloud rejection or cloud unavailability rejects the request.
30
+
31
+ See `@opensyber/tokenforge-protocol@2.0.0-rc.1` for the normative binary
32
+ contract, schemas, migration guide, and cross-language vectors.
33
+
34
+ ### Cloud v2 server boundary
35
+
36
+ Cloud v2 intentionally separates application authentication from TokenForge
37
+ device proof. The application owns the authenticated bind route and resolves
38
+ the current session on the server. Only that route may add a
39
+ `TokenForgeIdentity` envelope before delegating to the generic bind handler.
40
+ The browser never supplies trusted profile or subject fields.
41
+
42
+ ```ts
43
+ import {
44
+ createTokenForgeCloudBindHandler,
45
+ createTokenForgeCloudMiddleware,
46
+ } from '@opensyber/tokenforge/server';
47
+ import type { TokenForgeIdentity } from '@opensyber/tokenforge/identity';
48
+
49
+ // Configure these once in server-only application code. The generated types
50
+ // describe the required session resolver, replay store, cloud client, mode,
51
+ // telemetry, and circuit-breaker dependencies.
52
+ export const bindDevice = createTokenForgeCloudBindHandler(bindOptions);
53
+ export const protectRequest = createTokenForgeCloudMiddleware(verifyOptions);
54
+
55
+ // Your authenticated route resolves identity from its own session or IdP,
56
+ // then passes the standard Request to bindDevice. Never forward identity data
57
+ // accepted from the browser.
58
+ const identity: TokenForgeIdentity = await resolveIdentityFromSession(request);
59
+ ```
60
+
61
+ Framework adapters expose the same verification contract without duplicating
62
+ cryptography or authorization logic:
63
+
64
+ | Framework | Cloud v2 entry point | Request bridge |
65
+ |---|---|---|
66
+ | Next.js | `withTokenForgeCloudV2` / `tokenForgeCloudV2Check` | Native `Request` |
67
+ | Hono | `tokenForgeCloudV2Middleware` | `context.req.raw` |
68
+ | SvelteKit | `tokenForgeCloudV2Handle` | `event.request` |
69
+ | Astro | `tokenForgeCloudV2Middleware` | `context.request` |
70
+ | Express | `tokenForgeCloudV2Middleware` | Explicit `toRequest` required |
71
+ | Fastify | `tokenForgeCloudV2Plugin` | Explicit `toRequest` required |
72
+
73
+ Express and Fastify require an application-owned `toRequest` converter because
74
+ the SDK must not guess the external origin, proxy trust, or request-body
75
+ semantics. Conversion failure is rejected rather than silently bypassing
76
+ TokenForge.
77
+
78
+ The same named bind adapter is available from every framework entry point:
79
+
80
+ ```ts
81
+ import { createTokenForgeCloudBindHandler } from '@opensyber/tokenforge/server';
82
+ import { tokenForgeCloudV2BindRoute } from '@opensyber/tokenforge/nextjs';
83
+
84
+ const bindDevice = createTokenForgeCloudBindHandler(bindOptions);
85
+ export const POST = tokenForgeCloudV2BindRoute(bindDevice);
86
+ ```
87
+
88
+ For Hono, SvelteKit, and Astro, pass the same generic handler to
89
+ `tokenForgeCloudV2BindRoute`. Express and Fastify take a second argument with
90
+ the same explicit `toRequest` converter used by their verification adapter:
91
+
92
+ ```ts
93
+ app.post(
94
+ '/api/v1/auth/device/bind',
95
+ tokenForgeCloudV2BindRoute(bindDevice, { toRequest }),
96
+ );
97
+ ```
98
+
99
+ Mount this route only after the application's authentication middleware. The
100
+ generic bind handler must resolve the session and identity from trusted
101
+ server-side state; never copy subject or profile fields from the request body.
102
+
103
+ Recommended adoption order:
104
+
105
+ 1. Mount the authenticated bind route and verify identity correlation in
106
+ `off` mode.
107
+ 2. Enable `monitor` and inspect local denials, cloud would-block decisions,
108
+ outages, and replay telemetry without changing application availability.
109
+ 3. Resolve all unexplained monitor findings and switch selected protected paths
110
+ to `enforce`.
111
+ 4. Keep public health, callback, and webhook paths in the exact `skipPaths`
112
+ allowlist; do not use substring matching.
113
+
114
+ The older `tokenForgeMiddleware`, `tokenForgePlugin`, `withTokenForge`, and
115
+ `tokenForgeHandle` examples below use the legacy v1 fail-open cloud behavior.
116
+ They remain exported for staged migration but should not be selected for a new
117
+ Cloud v2 integration.
118
+
119
+ **Device-bound session security for the post-AiTM era.** W3C [DBSC](https://w3c.github.io/webappsec-dbsc/)–aligned, ECDSA P-256 + WebAuthn, drop-in for Auth0 / Okta / Clerk / Microsoft Entra ID. Every request after login is cryptographically signed with a device key that never leaves the browser. A stolen cookie without the device key is useless.
120
+
121
+ ## Why this matters in 2026
122
+
123
+ - **Session hijacking attacks grew 127% YoY** ([Microsoft, May 2026](https://www.microsoft.com/en-us/security/blog/2026/05/04/breaking-the-code-multi-stage-code-of-conduct-phishing-campaign-leads-to-aitm-token-compromise/))
124
+ - Adversary-in-the-Middle (AiTM) toolkits — EvilProxy, Tycoon — bypass MFA in real time
125
+ - Chrome 146 (April 2026) shipped browser-native DBSC for Windows; macOS/Linux pending
126
+ - Auth0 and Okta's "session protection" features rely on IP/ASN/UA fingerprinting — defeated by VPN-equipped attackers. **TokenForge uses cryptographic device binding — defeats them.**
4
127
 
5
128
  ## Quick Start
6
129
 
7
- ### 1. Add the script tag (client — zero code)
130
+ ### 1. Initialize after authenticated login
8
131
 
9
- ```html
10
- <script
11
- src="https://tokenforge-api.opensyber.cloud/sdk.js"
12
- data-api-key="tf_your_api_key"
13
- ></script>
132
+ ```ts
133
+ import { TokenForgeDevice } from '@opensyber/tokenforge/client';
134
+
135
+ const device = await TokenForgeDevice.open({
136
+ bindEndpoint: '/api/v1/auth/device/bind',
137
+ fetch: authenticatedFetch,
138
+ });
139
+ await device.bindAuthenticatedSession();
140
+ const secureFetch = device.wrapFetch(authenticatedFetch);
14
141
  ```
15
142
 
16
- That's it. The script auto-generates ECDSA P-256 device keys, binds the session, and signs every `fetch()` request with `X-TF-*` headers.
143
+ The browser receives no TokenForge API key. It generates a non-extractable
144
+ ECDSA P-256 device key, sends only the public JWK through the authenticated
145
+ application bind endpoint, and signs the exact request-bound v2 contract.
17
146
 
18
- ### 2. Add server middleware (one line)
147
+ ### 2. Add server middleware
19
148
 
20
149
  ```bash
21
150
  npm install @opensyber/tokenforge
22
151
  ```
23
152
 
24
- **Express**
25
153
  ```ts
154
+ // Legacy v1 Express migration path
26
155
  import { tokenForgeMiddleware } from '@opensyber/tokenforge/express';
27
156
  app.use(tokenForgeMiddleware({ apiKey: process.env.TOKENFORGE_API_KEY! }));
28
157
  // req.tf.bound, req.tf.trustScore, req.tf.deviceId
29
- ```
30
158
 
31
- **Next.js**
32
- ```ts
159
+ // Legacy v1 Next.js migration path
33
160
  import { withTokenForge } from '@opensyber/tokenforge/nextjs';
34
161
  export const GET = withTokenForge(handler, { apiKey: process.env.TOKENFORGE_API_KEY! });
35
- ```
36
162
 
37
- **Fastify**
38
- ```ts
163
+ // Legacy v1 Fastify migration path
39
164
  import { tokenForgePlugin } from '@opensyber/tokenforge/fastify';
40
165
  fastify.register(tokenForgePlugin, { apiKey: process.env.TOKENFORGE_API_KEY! });
41
- ```
42
166
 
43
- **Hono**
44
- ```ts
167
+ // Legacy v1 Hono migration path
45
168
  import { tokenForgeMiddleware } from '@opensyber/tokenforge/hono';
46
169
  app.use('/api/*', tokenForgeMiddleware({ apiKey: env.TOKENFORGE_API_KEY }));
47
170
  ```
48
171
 
49
172
  ### 3. Get your API key
50
173
 
51
- Sign up free at [tokenforge.opensyber.cloud](https://tokenforge.opensyber.cloud). Free tier: 1,000 verifications/month.
174
+ Sign up at [tokenforge.opensyber.cloud](https://tokenforge.opensyber.cloud) free tier: 1,000 verifications/month, no credit card.
52
175
 
53
- ## Why
176
+ ## What's new in v1.0.0 (May 2026)
54
177
 
55
- Authentication protects login. TokenForge protects everything **after** login:
178
+ This release graduates the protocol surface from beta. Subsequent 1.x is backward-compatible additions only.
56
179
 
57
- - **Session hijacking** stolen cookies are useless without the device's private key
58
- - **AiTM attacks** — relay proxies can't sign requests on behalf of the device
59
- - **Token theft** — XSS-exfiltrated tokens fail server-side signature verification
60
- - **Trust scoring** — 7 signals detect anomalies on every request in real time
180
+ ### W3C DBSC protocol (Sprint 37)
61
181
 
62
- ## How It Works
182
+ Aligned with the W3C draft + Chrome 146 native rollout:
183
+
184
+ ```
185
+ POST /v1/dbsc/challenge — issue one-shot challenge (register/refresh/step_up)
186
+ POST /v1/dbsc/register — bind device with JWS-signed challenge response
187
+ POST /v1/dbsc/refresh — rotate bound cookie, run risk policy
188
+ POST /v1/dbsc/sessions/:id/revoke — admin soft-revoke
189
+ GET /.well-known/tokenforge/jwks — public verifier keys (5-min edge cache)
190
+ GET /.well-known/tokenforge/dbsc — service descriptor for SDK auto-discovery
191
+ ```
192
+
193
+ ### Workforce SSO replaces Cisco Duo Premier (Sprint 36)
194
+
195
+ Five OIDC IdPs + SAML 2.0:
196
+
197
+ ```ts
198
+ import { exchangeSso } from '@opensyber/tokenforge/server/internal';
199
+
200
+ // After Okta / Entra / Google Workspace / Auth0 / generic OIDC login
201
+ const result = await exchangeSso(db, store, {
202
+ tenantId, workforceAppId,
203
+ idToken: req.body.idToken, // from your IdP
204
+ jwks, // cached via getJwks()
205
+ });
206
+ // result: { ok: true, subjectId, externalSubject, email, challenge, challengeExpiresAt }
207
+ ```
208
+
209
+ JWKS cache: 24-hour freshness with stale-fallback when IdP is unreachable. xmlsoap claim namespace for Microsoft AD FS / Azure AD compatibility built in.
210
+
211
+ ### AitM detection + per-route step-up (Sprint 39)
212
+
213
+ ```ts
214
+ import { requireFreshSig } from '@opensyber/tokenforge/server';
215
+
216
+ app.use('/admin/*', requireFreshSig({ minTrustScore: 90 }));
217
+ app.use('/billing/*', requireFreshSig({
218
+ minTrustScore: 95,
219
+ requireWebAuthn: true,
220
+ }));
221
+ ```
222
+
223
+ Per-tenant policy via `tf_tenants.step_up_actions` JSON:
224
+
225
+ ```json
226
+ [
227
+ { "path": "/admin/billing", "requireFreshSig": true, "freshSigMaxAgeSec": 30 },
228
+ { "path": "/admin/*", "requireFreshSig": true, "requireWebAuthn": true }
229
+ ]
230
+ ```
231
+
232
+ Exact match wins over glob. Glob /admin/* matches /admin/users but not /admin (no segment past prefix).
233
+
234
+ ### Action signing for sensitive operations
235
+
236
+ ```ts
237
+ // Client
238
+ const sig = await tokenforge.signAction({
239
+ action: 'transfer',
240
+ body: { fromAccount, toAccount, amount },
241
+ });
242
+ fetch('/api/transfer', {
243
+ method: 'POST',
244
+ headers: { 'X-TF-Action-Signature': sig },
245
+ body: JSON.stringify({ fromAccount, toAccount, amount }),
246
+ });
247
+ ```
248
+
249
+ 5-second freshness window. JWS claims include `actionHash` (SHA-256 over canonicalized body) so an attacker can't replay the signature with a different transfer amount.
250
+
251
+ ### Webhook event stream
252
+
253
+ 12 events, HMAC-SHA256 signed, retried with [1s, 4s, 15s] backoff, stable `X-TF-Delivery-Id` across retries:
254
+
255
+ ```
256
+ session.bound session.verified session.revoked
257
+ trust_score.degraded trust_score.critical session.hijack_attempt
258
+ usage.cap_exceeded dbsc.risk_signal dbsc.policy_block
259
+ dbsc.session_step_up dbsc.session_revoked webhook.test
260
+ ```
261
+
262
+ ```ts
263
+ import { verifyWebhookSignature } from '@opensyber/tokenforge/webhooks';
264
+
265
+ app.post('/webhooks/tokenforge', async (c) => {
266
+ const rawBody = await c.req.text();
267
+ const ok = await verifyWebhookSignature({
268
+ body: rawBody,
269
+ signatureHeader: c.req.header('X-TF-Signature') ?? '',
270
+ timestampHeader: c.req.header('X-TF-Timestamp') ?? '',
271
+ secret: c.env.TOKENFORGE_WEBHOOK_SECRET,
272
+ });
273
+ if (!ok) return c.json({ error: 'bad_signature' }, 401);
274
+ // handle event ...
275
+ });
276
+ ```
277
+
278
+ Secret rotation grace window: 24 hours. Send the new secret while the old one stays valid; receivers verify against either.
279
+
280
+ ## How it works
63
281
 
64
282
  ```
65
283
  Browser TokenForge API Your Server
66
284
  │ │ │
67
- │ 1. Script auto-generates │ │
68
- ECDSA P-256 key pair │ │
285
+ │ 1. Generate ECDSA P-256 │ │
286
+ keypair (non-extractable) │ │
287
+ │ │ │
288
+ │ 2. POST /v1/dbsc/challenge ───>│ │
289
+ │ <─────── { challenge } ────────│ │
69
290
  │ │ │
70
- 2. POST /v1/bind ─────────────>│ Store public key │
71
- <──────────────────────────────│ Return deviceId
291
+ 3. POST /v1/dbsc/register ────>│ Store public key │
292
+ + JWS over challenge
293
+ │ <─── { sessionId, deviceId } ──│ │
72
294
  │ │ │
73
- 3. fetch('/api/data') │ │
74
- │ + X-TF-Signature header ────────────────────────────────>
295
+ 4. fetch('/api/data') │ │
296
+ │ + X-TF-Signature ──────────────────────────────────────>
75
297
  │ │ │
76
298
  │ │ <── POST /v1/edge/verify ── │
77
299
  │ │ Verify signature │
78
- │ │ Check nonce replay
300
+ │ │ Run AitM heuristics
79
301
  │ │ Compute trust score │
80
- │ │ ── { allow, score: 92 } ──>
81
-
82
- │ <──────────────────────────────────── 200 OK ─────────────── │
302
+ │ │ ──> { allow, score: 92 }
303
+ <──────────────────────────────────── 200 OK ───────────────
83
304
  ```
84
305
 
85
- All verification runs on TokenForge's infrastructure. Your server sends request context, gets back allow/deny. No crypto libraries, no database, no storage to manage.
306
+ The device key never leaves the client. The TokenForge service holds the public key + session metadata. Your server passes request context, gets back an allow/step_up/block decision.
86
307
 
87
- ## Trust Score
88
-
89
- 7 signals scored 0-100 on every request:
308
+ ## Trust score signals
90
309
 
91
310
  | Signal | Weight | Detects |
92
311
  |--------|--------|---------|
93
- | Signature | 30 | Tampering, missing device key |
312
+ | Signature | 30 | Tampering, missing/wrong device key |
94
313
  | IP Address | 15 | IP change since binding |
95
314
  | Geo Location | 15 | Country mismatch |
96
- | Fingerprint | 15 | Browser fingerprint drift |
315
+ | Fingerprint | 15 | Browser/device fingerprint drift |
97
316
  | Velocity | 10 | Multiple IPs in short window |
98
- | Timing | 10 | Clock skew beyond tolerance |
317
+ | Timing | 10 | Clock skew beyond ±60s |
99
318
  | Nonce | 5 | Replay attacks |
100
319
 
101
- Score >= 80: allow. Score 40-79: step-up auth. Score < 40: block.
320
+ Score >= 80: `allow`. Score 40-79: `step_up`. Score < 40: `block`. Thresholds configurable per tenant.
321
+
322
+ ## Drop-in for your existing IdP
323
+
324
+ TokenForge runs *after* authentication — bring your own IdP:
102
325
 
103
- ## Advanced: npm Client SDK
326
+ | IdP | Integration | Built-in support |
327
+ |---|---|---|
328
+ | **Microsoft Entra ID** | Custom Authentication Extension webhook | Roadmap M11 (Q3 2026) |
329
+ | **Auth0** | Auth0 Action snippet | Roadmap M14 — Marketplace listing pending review |
330
+ | **Okta** | Inline Hook + Custom Authenticator | Roadmap M15 — OIN listing pending review |
331
+ | **Clerk** | `@tokenforge/clerk-middleware` shim | Roadmap M16 |
332
+ | **Auth.js / NextAuth** | Works today via `@opensyber/tokenforge/nextjs` | ✅ |
333
+ | **Firebase Auth** | Works today via `@opensyber/tokenforge/express` | ✅ |
334
+ | **Supabase Auth** | Works today via `@opensyber/tokenforge/express` | ✅ |
335
+ | **Custom JWT** | Works today | ✅ |
104
336
 
105
- For React apps or when you need more control than the script tag:
337
+ ## React integration
106
338
 
107
339
  ```tsx
108
340
  import { TokenForgeProvider, useTokenForge } from '@opensyber/tokenforge/react';
@@ -117,21 +349,66 @@ function App() {
117
349
  </TokenForgeProvider>
118
350
  );
119
351
  }
352
+
353
+ function ProtectedPage() {
354
+ const { bound, trustScore, deviceId } = useTokenForge();
355
+ if (!bound) return <BindButton />;
356
+ return <Dashboard trustScore={trustScore} deviceId={deviceId} />;
357
+ }
120
358
  ```
121
359
 
122
- ## Works With Any Auth Provider
360
+ ## Self-hosted server
123
361
 
124
- Microsoft Entra ID, Clerk, Auth.js, NextAuth, Firebase, Supabase, Okta, or custom JWT. TokenForge runs after authentication — no changes to your auth flow.
362
+ The server adapters work against your own database / KV — no hosted service required:
125
363
 
126
- ## Pricing
364
+ ```ts
365
+ import { createTokenForgeRoutes } from '@opensyber/tokenforge/server';
366
+ import { D1Storage } from '@opensyber/tokenforge/server/storage';
367
+ // or PostgresStorage, RedisStorage, or implement the StorageInterface
368
+
369
+ const tf = createTokenForgeRoutes({
370
+ storage: new D1Storage(env.DB),
371
+ sessionMaxAge: 86400,
372
+ });
373
+ app.route('/api/tf', tf);
374
+ ```
375
+
376
+ Storage backends shipped: D1 (Cloudflare Workers), PostgreSQL, Redis. Bring your own via `StorageInterface`.
377
+
378
+ ## Compatibility
379
+
380
+ | Platform | Status |
381
+ |---|---|
382
+ | Chrome 146+ Windows | ✅ Native DBSC + polyfill fallback |
383
+ | Chrome 146+ macOS / Linux | ⏳ DBSC pending Google rollout; polyfill works today |
384
+ | Safari (macOS / iOS) | ✅ Polyfill via Web Crypto + IndexedDB |
385
+ | Firefox | ✅ Polyfill |
386
+ | Edge 146+ | ✅ Inherits Chromium DBSC |
387
+ | Node.js 18+ (server) | ✅ |
388
+ | Bun, Deno | ✅ |
389
+ | Cloudflare Workers | ✅ |
390
+
391
+ ## Pricing (hosted service)
127
392
 
128
393
  | Plan | Price | Verifications/mo |
129
394
  |------|-------|-----------------|
130
395
  | Free | $0 | 1,000 |
131
396
  | Pro | $49/mo | 50,000 |
132
397
  | Team | $199/mo | 250,000 |
133
- | Enterprise | Custom | Unlimited |
398
+ | Enterprise | Custom | Unlimited + SLA |
399
+
400
+ The SDK code in this repository is MIT-licensed and runs against any storage backend — you can self-host without a TokenForge subscription.
401
+
402
+ ## Examples & docs
403
+
404
+ - Full API reference: [tokenforge.opensyber.cloud/docs](https://tokenforge.opensyber.cloud/docs)
405
+ - DBSC protocol explainer: [tokenforge.opensyber.cloud/dbsc](https://tokenforge.opensyber.cloud/dbsc)
406
+ - Reference apps (Next.js, Express, Hono, Fastify): [github.com/opensyber/tokenforge-examples](https://github.com/opensyber/tokenforge-examples)
407
+
408
+ ## Security disclosure
409
+
410
+ Security issues: `security@opensyber.cloud` (PGP key available). Coordinated disclosure within 90 days; CVE assignment for confirmed vulnerabilities.
134
411
 
135
412
  ## License
136
413
 
137
- Client SDK: MIT | Server verification requires a TokenForge API key from [tokenforge.opensyber.cloud](https://tokenforge.opensyber.cloud).
414
+ SDK: MIT (see [LICENSE.md](./LICENSE.md)). Hosted service requires API key governed by [TokenForge Terms](https://tokenforge.opensyber.cloud/terms).
@@ -0,0 +1,64 @@
1
+ import { type TokenForgeCloudV2AdapterOptions, type TokenForgeCloudV2RequestHandler } from './cloud-v2-bridge.js';
2
+ /** TokenForge context attached to context.locals.tf */
3
+ export interface TfContext {
4
+ bound: boolean;
5
+ trustScore: number;
6
+ deviceId: string | null;
7
+ }
8
+ /** Options for the Astro middleware. */
9
+ export interface TokenForgeAstroOptions {
10
+ /** Your TokenForge API key (starts with `tf_`). */
11
+ apiKey: string;
12
+ /** API base URL (defaults to TokenForge cloud). */
13
+ apiBase?: string;
14
+ /** Paths to skip verification. */
15
+ skipPaths?: string[];
16
+ }
17
+ /** Minimal Astro middleware context — avoids astro:middleware dep. */
18
+ interface APIContext {
19
+ request: Request;
20
+ url: URL;
21
+ locals: {
22
+ tf?: TfContext;
23
+ };
24
+ clientAddress?: string;
25
+ }
26
+ type MiddlewareNext = () => Promise<Response>;
27
+ type MiddlewareHandler = (context: APIContext, next: MiddlewareNext) => Promise<Response>;
28
+ /**
29
+ * Astro middleware for TokenForge verification via the cloud API.
30
+ *
31
+ * Usage:
32
+ * ```ts
33
+ * // src/middleware.ts
34
+ * import { tokenForgeMiddleware } from '@opensyber/tokenforge/astro';
35
+ * export const onRequest = tokenForgeMiddleware({
36
+ * apiKey: import.meta.env.TOKENFORGE_API_KEY,
37
+ * });
38
+ * ```
39
+ *
40
+ * Reads `Astro.locals.tf` for downstream pages and endpoints.
41
+ */
42
+ export declare function tokenForgeMiddleware(options: TokenForgeAstroOptions): MiddlewareHandler;
43
+ /**
44
+ * Per-route step-up gate. Use inside page or endpoint code after the
45
+ * middleware has populated `Astro.locals.tf`.
46
+ *
47
+ * Returns a 403 `Response` when below threshold; the caller should
48
+ * `return` it. Returns `null` on pass.
49
+ */
50
+ export declare function requireFreshSig(locals: {
51
+ tf?: TfContext;
52
+ }, opts?: {
53
+ minTrustScore?: number;
54
+ }): Response | null;
55
+ export type TokenForgeAstroCloudV2Options = TokenForgeCloudV2AdapterOptions;
56
+ /** Astro middleware backed by the shared fail-closed Cloud v2 contract. */
57
+ export declare function tokenForgeCloudV2Middleware(options: TokenForgeAstroCloudV2Options): MiddlewareHandler;
58
+ export interface TokenForgeAstroCloudV2RouteContext {
59
+ request: Request;
60
+ }
61
+ /** Adapt the generic Cloud v2 bind handler to an Astro API route. */
62
+ export declare function tokenForgeCloudV2BindRoute(handler: TokenForgeCloudV2RequestHandler): (context: TokenForgeAstroCloudV2RouteContext) => Promise<Response>;
63
+ export {};
64
+ //# sourceMappingURL=astro.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"astro.d.ts","sourceRoot":"../src/","sources":["adapters/astro.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,+BAA+B,EACpC,KAAK,+BAA+B,EACrC,MAAM,sBAAsB,CAAC;AAI9B,uDAAuD;AACvD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wCAAwC;AACxC,MAAM,WAAW,sBAAsB;IACrC,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,sEAAsE;AACtE,UAAU,UAAU;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE;QAAE,EAAE,CAAC,EAAE,SAAS,CAAA;KAAE,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,KAAK,cAAc,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC9C,KAAK,iBAAiB,GAAG,CACvB,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,cAAc,KACjB,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,sBAAsB,GAC9B,iBAAiB,CAoEnB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE;IAAE,EAAE,CAAC,EAAE,SAAS,CAAA;CAAE,EAC1B,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GACpC,QAAQ,GAAG,IAAI,CAcjB;AASD,MAAM,MAAM,6BAA6B,GAAG,+BAA+B,CAAC;AAE5E,2EAA2E;AAC3E,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,6BAA6B,GACrC,iBAAiB,CAOnB;AAED,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qEAAqE;AACrE,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,+BAA+B,IAGtC,SAAS,kCAAkC,KAC1C,OAAO,CAAC,QAAQ,CAAC,CACrB"}
@@ -0,0 +1,104 @@
1
+ import { createTokenForgeCloudV2Gate, } from './cloud-v2-bridge.js';
2
+ const DEFAULT_API_BASE = 'https://tokenforge-api.opensyber.cloud';
3
+ /**
4
+ * Astro middleware for TokenForge verification via the cloud API.
5
+ *
6
+ * Usage:
7
+ * ```ts
8
+ * // src/middleware.ts
9
+ * import { tokenForgeMiddleware } from '@opensyber/tokenforge/astro';
10
+ * export const onRequest = tokenForgeMiddleware({
11
+ * apiKey: import.meta.env.TOKENFORGE_API_KEY,
12
+ * });
13
+ * ```
14
+ *
15
+ * Reads `Astro.locals.tf` for downstream pages and endpoints.
16
+ */
17
+ export function tokenForgeMiddleware(options) {
18
+ const apiBase = options.apiBase ?? DEFAULT_API_BASE;
19
+ return async (context, next) => {
20
+ const path = context.url.pathname;
21
+ if (shouldSkip(path, options.skipPaths)) {
22
+ context.locals.tf = { bound: false, trustScore: 0, deviceId: null };
23
+ return next();
24
+ }
25
+ const header = (name) => context.request.headers.get(name);
26
+ try {
27
+ const apiRes = await fetch(`${apiBase}/v1/edge/verify`, {
28
+ method: 'POST',
29
+ headers: {
30
+ Authorization: `Bearer ${options.apiKey}`,
31
+ 'Content-Type': 'application/json',
32
+ },
33
+ body: JSON.stringify({
34
+ path,
35
+ method: context.request.method,
36
+ headers: {
37
+ signature: header('x-tf-signature'),
38
+ nonce: header('x-tf-nonce'),
39
+ timestamp: header('x-tf-timestamp'),
40
+ deviceId: header('x-tf-device-id'),
41
+ },
42
+ ipAddress: context.clientAddress ?? header('x-forwarded-for') ?? '',
43
+ countryCode: header('cf-ipcountry') ?? '',
44
+ userAgent: header('user-agent') ?? '',
45
+ }),
46
+ });
47
+ if (!apiRes.ok) {
48
+ context.locals.tf = { bound: false, trustScore: 0, deviceId: null };
49
+ return next();
50
+ }
51
+ const { data } = (await apiRes.json());
52
+ if (data.status === 'block') {
53
+ return new Response(JSON.stringify({ error: 'session_blocked', reason: data.reason }), { status: 401, headers: { 'Content-Type': 'application/json' } });
54
+ }
55
+ context.locals.tf = {
56
+ bound: data.bound,
57
+ trustScore: data.trustScore,
58
+ deviceId: data.deviceId,
59
+ };
60
+ return next();
61
+ }
62
+ catch {
63
+ context.locals.tf = { bound: false, trustScore: 0, deviceId: null };
64
+ return next();
65
+ }
66
+ };
67
+ }
68
+ /**
69
+ * Per-route step-up gate. Use inside page or endpoint code after the
70
+ * middleware has populated `Astro.locals.tf`.
71
+ *
72
+ * Returns a 403 `Response` when below threshold; the caller should
73
+ * `return` it. Returns `null` on pass.
74
+ */
75
+ export function requireFreshSig(locals, opts = {}) {
76
+ const min = opts.minTrustScore ?? 90;
77
+ const score = locals.tf?.trustScore ?? 0;
78
+ if (score < min) {
79
+ return new Response(JSON.stringify({
80
+ error: 'elevated_trust_required',
81
+ action: 'step_up_required',
82
+ trustScore: score,
83
+ }), { status: 403, headers: { 'Content-Type': 'application/json' } });
84
+ }
85
+ return null;
86
+ }
87
+ function shouldSkip(path, skipPaths) {
88
+ if (!skipPaths)
89
+ return false;
90
+ return skipPaths.some((p) => p.endsWith('*') ? path.startsWith(p.slice(0, -1)) : path === p);
91
+ }
92
+ /** Astro middleware backed by the shared fail-closed Cloud v2 contract. */
93
+ export function tokenForgeCloudV2Middleware(options) {
94
+ const gate = createTokenForgeCloudV2Gate(options);
95
+ return async (context, next) => {
96
+ const rejection = await gate(context.request);
97
+ return rejection ?? next();
98
+ };
99
+ }
100
+ /** Adapt the generic Cloud v2 bind handler to an Astro API route. */
101
+ export function tokenForgeCloudV2BindRoute(handler) {
102
+ return async (context) => handler(context.request);
103
+ }
104
+ //# sourceMappingURL=astro.js.map