@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.
- package/LICENSE.md +32 -0
- package/README.md +329 -52
- package/dist/adapters/astro.d.ts +64 -0
- package/dist/adapters/astro.d.ts.map +1 -0
- package/dist/adapters/astro.js +104 -0
- package/dist/adapters/astro.js.map +1 -0
- package/dist/adapters/cloud-v2-bridge.d.ts +8 -0
- package/dist/adapters/cloud-v2-bridge.d.ts.map +1 -0
- package/dist/adapters/cloud-v2-bridge.js +24 -0
- package/dist/adapters/cloud-v2-bridge.js.map +1 -0
- package/dist/adapters/express.d.ts +30 -2
- package/dist/adapters/express.d.ts.map +1 -1
- package/dist/adapters/express.js +65 -0
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/fastify.d.ts +32 -6
- package/dist/adapters/fastify.d.ts.map +1 -1
- package/dist/adapters/fastify.js +65 -0
- package/dist/adapters/fastify.js.map +1 -1
- package/dist/adapters/hono.d.ts +13 -1
- package/dist/adapters/hono.d.ts.map +1 -1
- package/dist/adapters/hono.js +14 -1
- package/dist/adapters/hono.js.map +1 -1
- package/dist/adapters/index.d.ts +5 -3
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +5 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/nextjs.d.ts +31 -0
- package/dist/adapters/nextjs.d.ts.map +1 -1
- package/dist/adapters/nextjs.js +83 -30
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/sveltekit.d.ts +66 -0
- package/dist/adapters/sveltekit.d.ts.map +1 -0
- package/dist/adapters/sveltekit.js +101 -0
- package/dist/adapters/sveltekit.js.map +1 -0
- package/dist/client/action-signer.d.ts +53 -0
- package/dist/client/action-signer.d.ts.map +1 -0
- package/dist/client/action-signer.js +48 -0
- package/dist/client/action-signer.js.map +1 -0
- package/dist/client/binding.d.ts +24 -7
- package/dist/client/binding.d.ts.map +1 -1
- package/dist/client/binding.js +17 -6
- package/dist/client/binding.js.map +1 -1
- package/dist/client/cloud-device.d.ts +54 -0
- package/dist/client/cloud-device.d.ts.map +1 -0
- package/dist/client/cloud-device.js +301 -0
- package/dist/client/cloud-device.js.map +1 -0
- package/dist/client/crypto.d.ts.map +1 -1
- package/dist/client/crypto.js.map +1 -1
- package/dist/client/index.d.ts +14 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +30 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/interceptor.d.ts.map +1 -1
- package/dist/client/interceptor.js +27 -3
- package/dist/client/interceptor.js.map +1 -1
- package/dist/client/signer.d.ts +40 -3
- package/dist/client/signer.d.ts.map +1 -1
- package/dist/client/signer.js +56 -5
- package/dist/client/signer.js.map +1 -1
- package/dist/client/storage.d.ts.map +1 -1
- package/dist/client/storage.js.map +1 -1
- package/dist/client/webauthn.d.ts +45 -0
- package/dist/client/webauthn.d.ts.map +1 -0
- package/dist/client/webauthn.js +118 -0
- package/dist/client/webauthn.js.map +1 -0
- package/dist/protocol/v2.d.ts +23 -0
- package/dist/protocol/v2.d.ts.map +1 -0
- package/dist/protocol/v2.js +157 -0
- package/dist/protocol/v2.js.map +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/provider.d.ts.map +1 -1
- package/dist/react/provider.js.map +1 -1
- package/dist/server/action-verify.d.ts +56 -0
- package/dist/server/action-verify.d.ts.map +1 -0
- package/dist/server/action-verify.js +47 -0
- package/dist/server/action-verify.js.map +1 -0
- package/dist/server/activity-histogram.d.ts +47 -0
- package/dist/server/activity-histogram.d.ts.map +1 -0
- package/dist/server/activity-histogram.js +76 -0
- package/dist/server/activity-histogram.js.map +1 -0
- package/dist/server/aitm-heuristics.d.ts +57 -0
- package/dist/server/aitm-heuristics.d.ts.map +1 -0
- package/dist/server/aitm-heuristics.js +120 -0
- package/dist/server/aitm-heuristics.js.map +1 -0
- package/dist/server/bind-with-id-token.d.ts +55 -0
- package/dist/server/bind-with-id-token.d.ts.map +1 -0
- package/dist/server/bind-with-id-token.js +70 -0
- package/dist/server/bind-with-id-token.js.map +1 -0
- package/dist/server/bound-cookie.d.ts +39 -0
- package/dist/server/bound-cookie.d.ts.map +1 -0
- package/dist/server/bound-cookie.js +59 -0
- package/dist/server/bound-cookie.js.map +1 -0
- package/dist/server/cbor.d.ts +20 -0
- package/dist/server/cbor.d.ts.map +1 -0
- package/dist/server/cbor.js +110 -0
- package/dist/server/cbor.js.map +1 -0
- package/dist/server/cloud-bind.d.ts +42 -0
- package/dist/server/cloud-bind.d.ts.map +1 -0
- package/dist/server/cloud-bind.js +387 -0
- package/dist/server/cloud-bind.js.map +1 -0
- package/dist/server/cloud-middleware.d.ts +46 -0
- package/dist/server/cloud-middleware.d.ts.map +1 -0
- package/dist/server/cloud-middleware.js +344 -0
- package/dist/server/cloud-middleware.js.map +1 -0
- package/dist/server/cloud-verify.d.ts +30 -0
- package/dist/server/cloud-verify.d.ts.map +1 -0
- package/dist/server/cloud-verify.js +206 -0
- package/dist/server/cloud-verify.js.map +1 -0
- package/dist/server/dbsc-challenge.d.ts +51 -0
- package/dist/server/dbsc-challenge.d.ts.map +1 -0
- package/dist/server/dbsc-challenge.js +78 -0
- package/dist/server/dbsc-challenge.js.map +1 -0
- package/dist/server/dbsc-endpoint.d.ts +20 -0
- package/dist/server/dbsc-endpoint.d.ts.map +1 -0
- package/dist/server/dbsc-endpoint.js +85 -0
- package/dist/server/dbsc-endpoint.js.map +1 -0
- package/dist/server/dbsc-verify.d.ts +32 -0
- package/dist/server/dbsc-verify.d.ts.map +1 -0
- package/dist/server/dbsc-verify.js +65 -0
- package/dist/server/dbsc-verify.js.map +1 -0
- package/dist/server/identity-profile.d.ts +18 -0
- package/dist/server/identity-profile.d.ts.map +1 -0
- package/dist/server/identity-profile.js +113 -0
- package/dist/server/identity-profile.js.map +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/jws-verify.d.ts +46 -0
- package/dist/server/jws-verify.d.ts.map +1 -0
- package/dist/server/jws-verify.js +64 -0
- package/dist/server/jws-verify.js.map +1 -0
- package/dist/server/middleware.d.ts +24 -0
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +87 -20
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/oidc-verify.d.ts +71 -0
- package/dist/server/oidc-verify.d.ts.map +1 -0
- package/dist/server/oidc-verify.js +115 -0
- package/dist/server/oidc-verify.js.map +1 -0
- package/dist/server/policy.d.ts +47 -0
- package/dist/server/policy.d.ts.map +1 -0
- package/dist/server/policy.js +84 -0
- package/dist/server/policy.js.map +1 -0
- package/dist/server/step-up-policy.d.ts +36 -0
- package/dist/server/step-up-policy.d.ts.map +1 -0
- package/dist/server/step-up-policy.js +101 -0
- package/dist/server/step-up-policy.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 +73 -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 +50 -0
- package/dist/server/storage/memory.d.ts.map +1 -0
- package/dist/server/storage/memory.js +121 -0
- package/dist/server/storage/memory.js.map +1 -0
- package/dist/server/webauthn-verify.d.ts +41 -0
- package/dist/server/webauthn-verify.d.ts.map +1 -0
- package/dist/server/webauthn-verify.js +175 -0
- package/dist/server/webauthn-verify.js.map +1 -0
- package/dist/server/webhooks.d.ts +42 -0
- package/dist/server/webhooks.d.ts.map +1 -0
- package/dist/server/webhooks.js +77 -0
- package/dist/server/webhooks.js.map +1 -0
- package/dist/shared/action-hash.d.ts +23 -0
- package/dist/shared/action-hash.d.ts.map +1 -0
- package/dist/shared/action-hash.js +35 -0
- package/dist/shared/action-hash.js.map +1 -0
- package/dist/shared/identity.d.ts +45 -0
- package/dist/shared/identity.d.ts.map +1 -0
- package/dist/shared/identity.js +176 -0
- package/dist/shared/identity.js.map +1 -0
- package/dist/shared/types-internal.d.ts.map +1 -1
- package/dist/shared/types-internal.js.map +1 -1
- package/dist/shared/types.d.ts +49 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +1 -0
- package/dist/shared/types.js.map +1 -1
- 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
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@opensyber/tokenforge)
|
|
4
|
+
[](./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.
|
|
130
|
+
### 1. Initialize after authenticated login
|
|
8
131
|
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
174
|
+
Sign up at [tokenforge.opensyber.cloud](https://tokenforge.opensyber.cloud) — free tier: 1,000 verifications/month, no credit card.
|
|
52
175
|
|
|
53
|
-
##
|
|
176
|
+
## What's new in v1.0.0 (May 2026)
|
|
54
177
|
|
|
55
|
-
|
|
178
|
+
This release graduates the protocol surface from beta. Subsequent 1.x is backward-compatible additions only.
|
|
56
179
|
|
|
57
|
-
|
|
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
|
-
|
|
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.
|
|
68
|
-
│
|
|
285
|
+
│ 1. Generate ECDSA P-256 │ │
|
|
286
|
+
│ keypair (non-extractable) │ │
|
|
287
|
+
│ │ │
|
|
288
|
+
│ 2. POST /v1/dbsc/challenge ───>│ │
|
|
289
|
+
│ <─────── { challenge } ────────│ │
|
|
69
290
|
│ │ │
|
|
70
|
-
│
|
|
71
|
-
│
|
|
291
|
+
│ 3. POST /v1/dbsc/register ────>│ Store public key │
|
|
292
|
+
│ + JWS over challenge │ │
|
|
293
|
+
│ <─── { sessionId, deviceId } ──│ │
|
|
72
294
|
│ │ │
|
|
73
|
-
│
|
|
74
|
-
│ + X-TF-Signature
|
|
295
|
+
│ 4. fetch('/api/data') │ │
|
|
296
|
+
│ + X-TF-Signature ──────────────────────────────────────> │
|
|
75
297
|
│ │ │
|
|
76
298
|
│ │ <── POST /v1/edge/verify ── │
|
|
77
299
|
│ │ Verify signature │
|
|
78
|
-
│ │
|
|
300
|
+
│ │ Run AitM heuristics │
|
|
79
301
|
│ │ Compute trust score │
|
|
80
|
-
│ │
|
|
81
|
-
│
|
|
82
|
-
│ <──────────────────────────────────── 200 OK ─────────────── │
|
|
302
|
+
│ │ ──> { allow, score: 92 } │
|
|
303
|
+
│ <──────────────────────────────────── 200 OK ─────────────── │
|
|
83
304
|
```
|
|
84
305
|
|
|
85
|
-
|
|
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
|
|
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
|
|
317
|
+
| Timing | 10 | Clock skew beyond ±60s |
|
|
99
318
|
| Nonce | 5 | Replay attacks |
|
|
100
319
|
|
|
101
|
-
Score >= 80: allow
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
360
|
+
## Self-hosted server
|
|
123
361
|
|
|
124
|
-
|
|
362
|
+
The server adapters work against your own database / KV — no hosted service required:
|
|
125
363
|
|
|
126
|
-
|
|
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
|
-
|
|
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
|