@kb-labs/gateway-auth 2.88.0 → 2.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +263 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,3 +1,265 @@
|
|
|
1
1
|
# @kb-labs/gateway-auth
|
|
2
2
|
|
|
3
|
-
Authentication and authorization middleware for the KB Labs API Gateway. Handles JWT
|
|
3
|
+
Authentication and authorization middleware for the KB Labs API Gateway. Handles client registration, JWT issuance, token refresh, and request authorization.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Auth Flow
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Client Gateway (/auth/*) ICache (store)
|
|
11
|
+
│ │ │
|
|
12
|
+
│ POST /auth/register │ │
|
|
13
|
+
│ { name, capabilities[] } │ │
|
|
14
|
+
├──────────────────────────────────►│ │
|
|
15
|
+
│ │ generateClientSecret() │
|
|
16
|
+
│ │ buildClientRecord() │
|
|
17
|
+
│ │ saveClient() ─────────────►│
|
|
18
|
+
│ │ (hash stored, never plain) │
|
|
19
|
+
│◄──────────────────────────────────┤ │
|
|
20
|
+
│ { clientId, clientSecret, │ │
|
|
21
|
+
│ hostId, namespaceId } │ │
|
|
22
|
+
│ (clientSecret returned ONCE) │ │
|
|
23
|
+
│ │ │
|
|
24
|
+
│ POST /auth/token │ │
|
|
25
|
+
│ { clientId, clientSecret } │ │
|
|
26
|
+
├──────────────────────────────────►│ │
|
|
27
|
+
│ │ verifyClientSecret() ──────►│
|
|
28
|
+
│ │ signAccessToken() │
|
|
29
|
+
│ │ signRefreshToken() │
|
|
30
|
+
│ │ saveRefreshToken() ────────►│
|
|
31
|
+
│◄──────────────────────────────────┤ │
|
|
32
|
+
│ { accessToken (15m), │ │
|
|
33
|
+
│ refreshToken (30d), │ │
|
|
34
|
+
│ expiresIn, tokenType } │ │
|
|
35
|
+
│ │ │
|
|
36
|
+
│ GET /any-route │ │
|
|
37
|
+
│ Authorization: Bearer <token> │ │
|
|
38
|
+
├──────────────────────────────────►│ │
|
|
39
|
+
│ │ verifyAccessToken() │
|
|
40
|
+
│ │ → AuthContext injected │
|
|
41
|
+
│ │ into request │
|
|
42
|
+
│◄──────────────────────────────────┤ │
|
|
43
|
+
│ 200 (or 401 if expired) │ │
|
|
44
|
+
│ │ │
|
|
45
|
+
│ POST /auth/refresh │ │
|
|
46
|
+
│ { refreshToken } │ │
|
|
47
|
+
├──────────────────────────────────►│ │
|
|
48
|
+
│ │ verifyRefreshToken() │
|
|
49
|
+
│ │ consumeRefreshToken() ─────►│ (old token deleted)
|
|
50
|
+
│ │ signAccessToken() │
|
|
51
|
+
│ │ signRefreshToken() │
|
|
52
|
+
│ │ saveRefreshToken() ────────►│ (new token saved)
|
|
53
|
+
│◄──────────────────────────────────┤ │
|
|
54
|
+
│ { accessToken (15m), │ │
|
|
55
|
+
│ refreshToken (30d, new) } │ │
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## API Endpoints
|
|
61
|
+
|
|
62
|
+
### `POST /auth/register`
|
|
63
|
+
|
|
64
|
+
Registers a new client. Returns credentials that grant access to the gateway.
|
|
65
|
+
|
|
66
|
+
**Request:**
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"name": "my-host-agent",
|
|
70
|
+
"capabilities": ["execution", "filesystem"]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Fields:
|
|
75
|
+
- `name` — human-readable label (stored, not validated for uniqueness)
|
|
76
|
+
- `capabilities` — list of capability strings the client declares
|
|
77
|
+
- `publicKey` _(optional)_ — PEM public key for asymmetric verification
|
|
78
|
+
|
|
79
|
+
**Response:**
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"clientId": "c_<hex>",
|
|
83
|
+
"clientSecret": "<random-secret>",
|
|
84
|
+
"hostId": "<uuid>",
|
|
85
|
+
"namespaceId": "<hex-16>"
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Security notes:
|
|
90
|
+
- `namespaceId` is **server-assigned** — never accept from client input
|
|
91
|
+
- `clientSecret` is returned **once** and never stored in plaintext (bcrypt hash in store)
|
|
92
|
+
- Save `clientSecret` immediately — it cannot be recovered
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### `POST /auth/token`
|
|
97
|
+
|
|
98
|
+
Exchanges `clientId` + `clientSecret` for a JWT token pair.
|
|
99
|
+
|
|
100
|
+
**Request:**
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"clientId": "c_<hex>",
|
|
104
|
+
"clientSecret": "<secret>"
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Response:**
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"accessToken": "<jwt>",
|
|
112
|
+
"refreshToken": "<jwt>",
|
|
113
|
+
"expiresIn": 900,
|
|
114
|
+
"tokenType": "Bearer"
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### `POST /auth/refresh`
|
|
121
|
+
|
|
122
|
+
Rotates the token pair using a valid refresh token. The old refresh token is **invalidated immediately** (single-use rotation).
|
|
123
|
+
|
|
124
|
+
**Request:**
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"refreshToken": "<jwt>"
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Response:** same shape as `/auth/token`.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## JWT Payload Schema
|
|
136
|
+
|
|
137
|
+
### Access Token (HS256, TTL: 15 minutes)
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
{
|
|
141
|
+
sub: string; // hostId
|
|
142
|
+
namespaceId: string; // tenant isolation key
|
|
143
|
+
tier: 'free' | 'pro' | 'enterprise';
|
|
144
|
+
type: 'machine' | 'user';
|
|
145
|
+
iat: number;
|
|
146
|
+
exp: number;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Refresh Token (HS256, TTL: 30 days)
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
{
|
|
154
|
+
sub: string; // hostId
|
|
155
|
+
type: 'refresh';
|
|
156
|
+
jti: string; // UUID — used as store key for rotation
|
|
157
|
+
iat: number;
|
|
158
|
+
exp: number;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## AuthContext
|
|
165
|
+
|
|
166
|
+
Every authenticated request gets an `AuthContext` injected into the Fastify request object:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
interface AuthContext {
|
|
170
|
+
type: 'machine' | 'user';
|
|
171
|
+
userId: string; // hostId from JWT sub
|
|
172
|
+
namespaceId: string; // tenant isolation key
|
|
173
|
+
tier: 'free' | 'pro' | 'enterprise';
|
|
174
|
+
permissions: string[];
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Access in route handlers:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
fastify.get('/my-route', async (request) => {
|
|
182
|
+
const auth = request.authContext; // AuthContext | undefined
|
|
183
|
+
if (!auth) { return reply.code(401).send(); }
|
|
184
|
+
// auth.namespaceId — use for all data scoping
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Routes under `/internal/*` use `x-internal-secret` header instead of JWT — they are not accessible from the public internet.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Token Lifecycle
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
Registration
|
|
196
|
+
└─ clientSecret: bcrypt-hashed in store, plaintext returned once
|
|
197
|
+
|
|
198
|
+
Access Token (15 min)
|
|
199
|
+
└─ Short-lived. On expiry → 401. Client must refresh.
|
|
200
|
+
|
|
201
|
+
Refresh Token (30 days)
|
|
202
|
+
└─ Single-use rotation: each /auth/refresh invalidates old token, issues new one.
|
|
203
|
+
└─ Stored by jti (UUID) in ICache with TTL.
|
|
204
|
+
└─ If token is replayed after rotation → consumeRefreshToken returns null → 401.
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Security Properties
|
|
210
|
+
|
|
211
|
+
| Property | Implementation |
|
|
212
|
+
|----------|---------------|
|
|
213
|
+
| `clientSecret` never stored plaintext | bcrypt hash in store (`saveClient`) |
|
|
214
|
+
| `namespaceId` server-assigned | Generated via `randomBytes(16).toString('hex')` at registration |
|
|
215
|
+
| Refresh token rotation | `consumeRefreshToken` atomically deletes old token before issuing new one |
|
|
216
|
+
| JWT tokens not logged | `redactQueryToken()` strips `?access_token=` from access logs |
|
|
217
|
+
| CORS not reflected | Gateway sets `origin: false` — no `Access-Control-Allow-Origin` echo |
|
|
218
|
+
| Internal routes auth | `x-internal-secret` header required, checked before JWT middleware |
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Usage Example
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# Register
|
|
226
|
+
CREDS=$(curl -s -X POST http://localhost:4000/auth/register \
|
|
227
|
+
-H "Content-Type: application/json" \
|
|
228
|
+
-d '{"name":"my-agent","capabilities":[]}')
|
|
229
|
+
|
|
230
|
+
CLIENT_ID=$(echo $CREDS | jq -r '.clientId')
|
|
231
|
+
CLIENT_SECRET=$(echo $CREDS | jq -r '.clientSecret')
|
|
232
|
+
|
|
233
|
+
# Get token
|
|
234
|
+
TOKEN=$(curl -s -X POST http://localhost:4000/auth/token \
|
|
235
|
+
-H "Content-Type: application/json" \
|
|
236
|
+
-d "{\"clientId\":\"$CLIENT_ID\",\"clientSecret\":\"$CLIENT_SECRET\"}" \
|
|
237
|
+
| jq -r '.accessToken')
|
|
238
|
+
|
|
239
|
+
# Use token
|
|
240
|
+
curl -s http://localhost:4000/health \
|
|
241
|
+
-H "Authorization: Bearer $TOKEN" | jq .
|
|
242
|
+
|
|
243
|
+
# Refresh (when access token expires)
|
|
244
|
+
REFRESH=$(curl -s -X POST http://localhost:4000/auth/refresh \
|
|
245
|
+
-H "Content-Type: application/json" \
|
|
246
|
+
-d "{\"refreshToken\":\"$REFRESH_TOKEN\"}" | jq -r '.accessToken')
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Configuration
|
|
252
|
+
|
|
253
|
+
JWT secret is required in production — gateway throws a fatal error at startup if `GATEWAY_JWT_SECRET` is not set when `NODE_ENV=production`.
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# Generate a strong secret
|
|
257
|
+
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Set via environment variable:
|
|
261
|
+
```bash
|
|
262
|
+
GATEWAY_JWT_SECRET=<64-byte-hex> kb-dev start gateway
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
See [Architecture Guide](../../../../docs/ARCHITECTURE-GUIDE.md) for full environment variable reference.
|
package/dist/index.d.ts
CHANGED
|
@@ -73,10 +73,10 @@ declare function getClient(cache: ICache, clientId: string): Promise<ClientRecor
|
|
|
73
73
|
declare function verifyClientSecret(cache: ICache, clientId: string, secret: string): Promise<ClientRecord | null>;
|
|
74
74
|
declare function buildClientRecord(opts: {
|
|
75
75
|
name: string;
|
|
76
|
-
namespaceId: string;
|
|
77
76
|
capabilities: string[];
|
|
78
77
|
publicKey?: string;
|
|
79
78
|
secret: string;
|
|
79
|
+
namespaceId?: string;
|
|
80
80
|
}): ClientRecord;
|
|
81
81
|
declare function saveRefreshToken(cache: ICache, token: string, hostId: string, namespaceId: string): Promise<void>;
|
|
82
82
|
declare function consumeRefreshToken(cache: ICache, token: string): Promise<{
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,9 @@ function generateClientSecret() {
|
|
|
18
18
|
function generateHostId() {
|
|
19
19
|
return `host_${randomBytes(12).toString("hex")}`;
|
|
20
20
|
}
|
|
21
|
+
function generateNamespaceId() {
|
|
22
|
+
return `ns_${randomBytes(16).toString("hex")}`;
|
|
23
|
+
}
|
|
21
24
|
async function saveClient(cache, record) {
|
|
22
25
|
await cache.set(`auth:client:${record.clientId}`, record);
|
|
23
26
|
await cache.set(`auth:hostindex:${record.hostId}`, record.clientId);
|
|
@@ -47,7 +50,7 @@ function buildClientRecord(opts) {
|
|
|
47
50
|
clientId: generateClientId(),
|
|
48
51
|
secretHash: hashSecret(opts.secret),
|
|
49
52
|
hostId: generateHostId(),
|
|
50
|
-
namespaceId: opts.namespaceId,
|
|
53
|
+
namespaceId: opts.namespaceId ?? generateNamespaceId(),
|
|
51
54
|
tier: "free",
|
|
52
55
|
name: opts.name,
|
|
53
56
|
capabilities: opts.capabilities,
|
|
@@ -132,7 +135,6 @@ var AuthService = class {
|
|
|
132
135
|
const secret = generateClientSecret();
|
|
133
136
|
const record = buildClientRecord({
|
|
134
137
|
name: req.name,
|
|
135
|
-
namespaceId: req.namespaceId,
|
|
136
138
|
capabilities: req.capabilities ?? [],
|
|
137
139
|
publicKey: req.publicKey,
|
|
138
140
|
secret
|
|
@@ -145,7 +147,8 @@ var AuthService = class {
|
|
|
145
147
|
clientId: record.clientId,
|
|
146
148
|
clientSecret: secret,
|
|
147
149
|
// returned ONCE, never stored in plaintext
|
|
148
|
-
hostId: record.hostId
|
|
150
|
+
hostId: record.hostId,
|
|
151
|
+
namespaceId: record.namespaceId
|
|
149
152
|
};
|
|
150
153
|
}
|
|
151
154
|
// ── Issue token pair ──────────────────────────────────────────────────────
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/store.ts","../src/jwt.ts","../src/service.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Auth storage — ICache operations for credentials and tokens.\n *\n * Key schema:\n * auth:client:{clientId} → ClientRecord (permanent)\n * auth:refresh:{tokenHash} → { hostId } (TTL 30d)\n * auth:publickey:{hostId} → string (base64url X25519 public key)\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\nimport type { ICache } from '@kb-labs/core-platform';\n\nconst REFRESH_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days\n\nexport interface ClientRecord {\n clientId: string;\n /** bcrypt-style: we store sha256 hash of the secret for simplicity (no bcrypt dep) */\n secretHash: string;\n hostId: string;\n namespaceId: string;\n tier: 'free' | 'pro' | 'enterprise';\n name: string;\n capabilities: string[];\n publicKey?: string;\n createdAt: number;\n}\n\nfunction hashSecret(secret: string): string {\n return createHash('sha256').update(secret).digest('hex');\n}\n\nfunction hashToken(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\nexport function generateClientId(): string {\n return `clt_${randomBytes(16).toString('hex')}`;\n}\n\nexport function generateClientSecret(): string {\n return `cs_${randomBytes(32).toString('base64url')}`;\n}\n\nexport function generateHostId(): string {\n return `host_${randomBytes(12).toString('hex')}`;\n}\n\n// ── Client CRUD ───────────────────────────────────────────────────────────────\n\nexport async function saveClient(cache: ICache, record: ClientRecord): Promise<void> {\n await cache.set(`auth:client:${record.clientId}`, record);\n // Reverse index: hostId → clientId for WS handler lookups\n await cache.set(`auth:hostindex:${record.hostId}`, record.clientId);\n}\n\nexport async function getClientByHostId(cache: ICache, hostId: string): Promise<ClientRecord | null> {\n const clientId = await cache.get<string>(`auth:hostindex:${hostId}`);\n if (!clientId) {return null;}\n return getClient(cache, clientId);\n}\n\nexport async function getClient(cache: ICache, clientId: string): Promise<ClientRecord | null> {\n return cache.get<ClientRecord>(`auth:client:${clientId}`);\n}\n\nexport async function verifyClientSecret(\n cache: ICache,\n clientId: string,\n secret: string,\n): Promise<ClientRecord | null> {\n const record = await getClient(cache, clientId);\n if (!record) {return null;}\n if (record.secretHash !== hashSecret(secret)) {return null;}\n return record;\n}\n\nexport function buildClientRecord(opts: {\n name: string;\n namespaceId: string;\n capabilities: string[];\n publicKey?: string;\n secret: string;\n}): ClientRecord {\n return {\n clientId: generateClientId(),\n secretHash: hashSecret(opts.secret),\n hostId: generateHostId(),\n namespaceId: opts.namespaceId,\n tier: 'free',\n name: opts.name,\n capabilities: opts.capabilities,\n publicKey: opts.publicKey,\n createdAt: Date.now(),\n };\n}\n\n// ── Refresh tokens ────────────────────────────────────────────────────────────\n\nexport async function saveRefreshToken(\n cache: ICache,\n token: string,\n hostId: string,\n namespaceId: string,\n): Promise<void> {\n await cache.set(`auth:refresh:${hashToken(token)}`, { hostId, namespaceId }, REFRESH_TTL_MS);\n}\n\nexport async function consumeRefreshToken(\n cache: ICache,\n token: string,\n): Promise<{ hostId: string; namespaceId: string } | null> {\n const key = `auth:refresh:${hashToken(token)}`;\n const entry = await cache.get<{ hostId: string; namespaceId: string }>(key);\n if (!entry) {return null;}\n // Rotate: delete old token (new one will be issued)\n await cache.delete(key);\n return entry;\n}\n\n// ── Public keys (E2E encryption) ──────────────────────────────────────────────\n\nexport async function savePublicKey(\n cache: ICache,\n hostId: string,\n publicKey: string,\n): Promise<void> {\n await cache.set(`auth:publickey:${hostId}`, publicKey);\n}\n\nexport async function getPublicKey(\n cache: ICache,\n hostId: string,\n): Promise<string | null> {\n return cache.get<string>(`auth:publickey:${hostId}`);\n}\n","/**\n * JWT sign / verify / refresh logic.\n * Pure functions — no side effects, no ICache dependency.\n */\n\nimport { SignJWT, jwtVerify, type JWTPayload } from 'jose';\nimport { randomUUID } from 'node:crypto';\nimport type { JwtPayload, TokenType } from '@kb-labs/gateway-contracts';\n\nconst ACCESS_TOKEN_TTL = 15 * 60; // 15 minutes in seconds\nconst REFRESH_TOKEN_TTL = 30 * 24 * 3600; // 30 days in seconds\n\nexport interface JwtConfig {\n /** Raw secret string — converted to TextEncoder internally */\n secret: string;\n}\n\nfunction getSecretKey(secret: string): Uint8Array {\n return new TextEncoder().encode(secret);\n}\n\nexport interface SignAccessTokenOptions {\n hostId: string;\n namespaceId: string;\n tier: 'free' | 'pro' | 'enterprise';\n type: TokenType;\n}\n\nexport async function signAccessToken(\n opts: SignAccessTokenOptions,\n config: JwtConfig,\n): Promise<{ token: string; expiresIn: number }> {\n const key = getSecretKey(config.secret);\n const now = Math.floor(Date.now() / 1000);\n\n const token = await new SignJWT({\n namespaceId: opts.namespaceId,\n tier: opts.tier,\n type: opts.type,\n })\n .setProtectedHeader({ alg: 'HS256' })\n .setSubject(opts.hostId)\n .setIssuedAt(now)\n .setExpirationTime(now + ACCESS_TOKEN_TTL)\n .sign(key);\n\n return { token, expiresIn: ACCESS_TOKEN_TTL };\n}\n\nexport async function signRefreshToken(\n hostId: string,\n config: JwtConfig,\n): Promise<string> {\n const key = getSecretKey(config.secret);\n const now = Math.floor(Date.now() / 1000);\n const jti = randomUUID();\n\n return new SignJWT({ type: 'refresh', jti })\n .setProtectedHeader({ alg: 'HS256' })\n .setSubject(hostId)\n .setIssuedAt(now)\n .setExpirationTime(now + REFRESH_TOKEN_TTL)\n .sign(key);\n}\n\nexport async function verifyAccessToken(\n token: string,\n config: JwtConfig,\n): Promise<JwtPayload | null> {\n try {\n const key = getSecretKey(config.secret);\n const { payload } = await jwtVerify(token, key);\n return payload as unknown as JwtPayload;\n } catch {\n return null;\n }\n}\n\nexport async function verifyRefreshToken(\n token: string,\n config: JwtConfig,\n): Promise<{ hostId: string } | null> {\n try {\n const key = getSecretKey(config.secret);\n const { payload } = await jwtVerify(token, key);\n if ((payload as JWTPayload & { type?: string }).type !== 'refresh') {\n return null;\n }\n if (!payload.sub) {return null;}\n return { hostId: payload.sub };\n } catch {\n return null;\n }\n}\n","/**\n * AuthService — orchestrates registration, token issuance, and refresh.\n * Depends on ICache (via store) and JWT config.\n */\n\nimport type { ICache } from '@kb-labs/core-platform';\nimport type {\n RegisterRequest,\n RegisterResponse,\n TokenResponse,\n AuthContext,\n} from '@kb-labs/gateway-contracts';\nimport {\n buildClientRecord,\n generateClientSecret,\n saveClient,\n verifyClientSecret,\n saveRefreshToken,\n consumeRefreshToken,\n savePublicKey,\n} from './store.js';\nimport {\n signAccessToken,\n signRefreshToken,\n verifyAccessToken,\n verifyRefreshToken,\n type JwtConfig,\n} from './jwt.js';\n\nexport class AuthService {\n constructor(\n private readonly cache: ICache,\n private readonly jwtConfig: JwtConfig,\n ) {}\n\n // ── Register ──────────────────────────────────────────────────────────────\n\n async register(req: RegisterRequest): Promise<RegisterResponse & { clientSecret: string }> {\n const secret = generateClientSecret();\n const record = buildClientRecord({\n name: req.name,\n namespaceId: req.namespaceId,\n capabilities: req.capabilities ?? [],\n publicKey: req.publicKey,\n secret,\n });\n\n await saveClient(this.cache, record);\n\n if (req.publicKey) {\n await savePublicKey(this.cache, record.hostId, req.publicKey);\n }\n\n return {\n clientId: record.clientId,\n clientSecret: secret, // returned ONCE, never stored in plaintext\n hostId: record.hostId,\n };\n }\n\n // ── Issue token pair ──────────────────────────────────────────────────────\n\n async issueTokens(clientId: string, clientSecret: string): Promise<TokenResponse | null> {\n const record = await verifyClientSecret(this.cache, clientId, clientSecret);\n if (!record) {return null;}\n\n const [{ token: accessToken, expiresIn }, refreshToken] = await Promise.all([\n signAccessToken(\n {\n hostId: record.hostId,\n namespaceId: record.namespaceId,\n tier: record.tier,\n type: 'machine',\n },\n this.jwtConfig,\n ),\n signRefreshToken(record.hostId, this.jwtConfig),\n ]);\n\n await saveRefreshToken(this.cache, refreshToken, record.hostId, record.namespaceId);\n\n return { accessToken, refreshToken, expiresIn, tokenType: 'Bearer' };\n }\n\n // ── Refresh token pair ────────────────────────────────────────────────────\n\n async refreshTokens(refreshToken: string): Promise<TokenResponse | null> {\n // Verify JWT signature first\n const jwtResult = await verifyRefreshToken(refreshToken, this.jwtConfig);\n if (!jwtResult) {return null;}\n\n // Consume from store (rotation — old token invalidated)\n const stored = await consumeRefreshToken(this.cache, refreshToken);\n if (!stored) {return null;}\n\n // Reload client record to get current tier/namespace\n // We need to scan by hostId — store clientId → hostId mapping for lookup\n // For now: resolve via a secondary key written at registration\n const hostId = stored.hostId;\n\n // Issue new pair\n const [{ token: accessToken, expiresIn }, newRefreshToken] = await Promise.all([\n signAccessToken(\n { hostId, namespaceId: stored.namespaceId, tier: 'free', type: 'machine' },\n this.jwtConfig,\n ),\n signRefreshToken(hostId, this.jwtConfig),\n ]);\n\n await saveRefreshToken(this.cache, newRefreshToken, hostId, stored.namespaceId);\n\n return { accessToken, refreshToken: newRefreshToken, expiresIn, tokenType: 'Bearer' };\n }\n\n // ── Verify access token → AuthContext ────────────────────────────────────\n\n async verify(token: string): Promise<AuthContext | null> {\n const payload = await verifyAccessToken(token, this.jwtConfig);\n if (!payload) {return null;}\n\n return {\n type: payload.type,\n userId: payload.sub,\n namespaceId: payload.namespaceId,\n tier: payload.tier,\n permissions: ['host:connect'],\n };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/store.ts","../src/jwt.ts","../src/service.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Auth storage — ICache operations for credentials and tokens.\n *\n * Key schema:\n * auth:client:{clientId} → ClientRecord (permanent)\n * auth:refresh:{tokenHash} → { hostId } (TTL 30d)\n * auth:publickey:{hostId} → string (base64url X25519 public key)\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\nimport type { ICache } from '@kb-labs/core-platform';\n\nconst REFRESH_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days\n\nexport interface ClientRecord {\n clientId: string;\n /** bcrypt-style: we store sha256 hash of the secret for simplicity (no bcrypt dep) */\n secretHash: string;\n hostId: string;\n namespaceId: string;\n tier: 'free' | 'pro' | 'enterprise';\n name: string;\n capabilities: string[];\n publicKey?: string;\n createdAt: number;\n}\n\nfunction hashSecret(secret: string): string {\n return createHash('sha256').update(secret).digest('hex');\n}\n\nfunction hashToken(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\nexport function generateClientId(): string {\n return `clt_${randomBytes(16).toString('hex')}`;\n}\n\nexport function generateClientSecret(): string {\n return `cs_${randomBytes(32).toString('base64url')}`;\n}\n\nexport function generateHostId(): string {\n return `host_${randomBytes(12).toString('hex')}`;\n}\n\nexport function generateNamespaceId(): string {\n return `ns_${randomBytes(16).toString('hex')}`;\n}\n\n// ── Client CRUD ───────────────────────────────────────────────────────────────\n\nexport async function saveClient(cache: ICache, record: ClientRecord): Promise<void> {\n await cache.set(`auth:client:${record.clientId}`, record);\n // Reverse index: hostId → clientId for WS handler lookups\n await cache.set(`auth:hostindex:${record.hostId}`, record.clientId);\n}\n\nexport async function getClientByHostId(cache: ICache, hostId: string): Promise<ClientRecord | null> {\n const clientId = await cache.get<string>(`auth:hostindex:${hostId}`);\n if (!clientId) {return null;}\n return getClient(cache, clientId);\n}\n\nexport async function getClient(cache: ICache, clientId: string): Promise<ClientRecord | null> {\n return cache.get<ClientRecord>(`auth:client:${clientId}`);\n}\n\nexport async function verifyClientSecret(\n cache: ICache,\n clientId: string,\n secret: string,\n): Promise<ClientRecord | null> {\n const record = await getClient(cache, clientId);\n if (!record) {return null;}\n if (record.secretHash !== hashSecret(secret)) {return null;}\n return record;\n}\n\nexport function buildClientRecord(opts: {\n name: string;\n capabilities: string[];\n publicKey?: string;\n secret: string;\n namespaceId?: string;\n}): ClientRecord {\n return {\n clientId: generateClientId(),\n secretHash: hashSecret(opts.secret),\n hostId: generateHostId(),\n namespaceId: opts.namespaceId ?? generateNamespaceId(),\n tier: 'free',\n name: opts.name,\n capabilities: opts.capabilities,\n publicKey: opts.publicKey,\n createdAt: Date.now(),\n };\n}\n\n// ── Refresh tokens ────────────────────────────────────────────────────────────\n\nexport async function saveRefreshToken(\n cache: ICache,\n token: string,\n hostId: string,\n namespaceId: string,\n): Promise<void> {\n await cache.set(`auth:refresh:${hashToken(token)}`, { hostId, namespaceId }, REFRESH_TTL_MS);\n}\n\nexport async function consumeRefreshToken(\n cache: ICache,\n token: string,\n): Promise<{ hostId: string; namespaceId: string } | null> {\n const key = `auth:refresh:${hashToken(token)}`;\n const entry = await cache.get<{ hostId: string; namespaceId: string }>(key);\n if (!entry) {return null;}\n // Rotate: delete old token (new one will be issued)\n await cache.delete(key);\n return entry;\n}\n\n// ── Public keys (E2E encryption) ──────────────────────────────────────────────\n\nexport async function savePublicKey(\n cache: ICache,\n hostId: string,\n publicKey: string,\n): Promise<void> {\n await cache.set(`auth:publickey:${hostId}`, publicKey);\n}\n\nexport async function getPublicKey(\n cache: ICache,\n hostId: string,\n): Promise<string | null> {\n return cache.get<string>(`auth:publickey:${hostId}`);\n}\n","/**\n * JWT sign / verify / refresh logic.\n * Pure functions — no side effects, no ICache dependency.\n */\n\nimport { SignJWT, jwtVerify, type JWTPayload } from 'jose';\nimport { randomUUID } from 'node:crypto';\nimport type { JwtPayload, TokenType } from '@kb-labs/gateway-contracts';\n\nconst ACCESS_TOKEN_TTL = 15 * 60; // 15 minutes in seconds\nconst REFRESH_TOKEN_TTL = 30 * 24 * 3600; // 30 days in seconds\n\nexport interface JwtConfig {\n /** Raw secret string — converted to TextEncoder internally */\n secret: string;\n}\n\nfunction getSecretKey(secret: string): Uint8Array {\n return new TextEncoder().encode(secret);\n}\n\nexport interface SignAccessTokenOptions {\n hostId: string;\n namespaceId: string;\n tier: 'free' | 'pro' | 'enterprise';\n type: TokenType;\n}\n\nexport async function signAccessToken(\n opts: SignAccessTokenOptions,\n config: JwtConfig,\n): Promise<{ token: string; expiresIn: number }> {\n const key = getSecretKey(config.secret);\n const now = Math.floor(Date.now() / 1000);\n\n const token = await new SignJWT({\n namespaceId: opts.namespaceId,\n tier: opts.tier,\n type: opts.type,\n })\n .setProtectedHeader({ alg: 'HS256' })\n .setSubject(opts.hostId)\n .setIssuedAt(now)\n .setExpirationTime(now + ACCESS_TOKEN_TTL)\n .sign(key);\n\n return { token, expiresIn: ACCESS_TOKEN_TTL };\n}\n\nexport async function signRefreshToken(\n hostId: string,\n config: JwtConfig,\n): Promise<string> {\n const key = getSecretKey(config.secret);\n const now = Math.floor(Date.now() / 1000);\n const jti = randomUUID();\n\n return new SignJWT({ type: 'refresh', jti })\n .setProtectedHeader({ alg: 'HS256' })\n .setSubject(hostId)\n .setIssuedAt(now)\n .setExpirationTime(now + REFRESH_TOKEN_TTL)\n .sign(key);\n}\n\nexport async function verifyAccessToken(\n token: string,\n config: JwtConfig,\n): Promise<JwtPayload | null> {\n try {\n const key = getSecretKey(config.secret);\n const { payload } = await jwtVerify(token, key);\n return payload as unknown as JwtPayload;\n } catch {\n return null;\n }\n}\n\nexport async function verifyRefreshToken(\n token: string,\n config: JwtConfig,\n): Promise<{ hostId: string } | null> {\n try {\n const key = getSecretKey(config.secret);\n const { payload } = await jwtVerify(token, key);\n if ((payload as JWTPayload & { type?: string }).type !== 'refresh') {\n return null;\n }\n if (!payload.sub) {return null;}\n return { hostId: payload.sub };\n } catch {\n return null;\n }\n}\n","/**\n * AuthService — orchestrates registration, token issuance, and refresh.\n * Depends on ICache (via store) and JWT config.\n */\n\nimport type { ICache } from '@kb-labs/core-platform';\nimport type {\n RegisterRequest,\n RegisterResponse,\n TokenResponse,\n AuthContext,\n} from '@kb-labs/gateway-contracts';\nimport {\n buildClientRecord,\n generateClientSecret,\n saveClient,\n verifyClientSecret,\n saveRefreshToken,\n consumeRefreshToken,\n savePublicKey,\n} from './store.js';\nimport {\n signAccessToken,\n signRefreshToken,\n verifyAccessToken,\n verifyRefreshToken,\n type JwtConfig,\n} from './jwt.js';\n\nexport class AuthService {\n constructor(\n private readonly cache: ICache,\n private readonly jwtConfig: JwtConfig,\n ) {}\n\n // ── Register ──────────────────────────────────────────────────────────────\n\n async register(req: RegisterRequest): Promise<RegisterResponse & { clientSecret: string }> {\n const secret = generateClientSecret();\n const record = buildClientRecord({\n name: req.name,\n capabilities: req.capabilities ?? [],\n publicKey: req.publicKey,\n secret,\n });\n\n await saveClient(this.cache, record);\n\n if (req.publicKey) {\n await savePublicKey(this.cache, record.hostId, req.publicKey);\n }\n\n return {\n clientId: record.clientId,\n clientSecret: secret, // returned ONCE, never stored in plaintext\n hostId: record.hostId,\n namespaceId: record.namespaceId,\n };\n }\n\n // ── Issue token pair ──────────────────────────────────────────────────────\n\n async issueTokens(clientId: string, clientSecret: string): Promise<TokenResponse | null> {\n const record = await verifyClientSecret(this.cache, clientId, clientSecret);\n if (!record) {return null;}\n\n const [{ token: accessToken, expiresIn }, refreshToken] = await Promise.all([\n signAccessToken(\n {\n hostId: record.hostId,\n namespaceId: record.namespaceId,\n tier: record.tier,\n type: 'machine',\n },\n this.jwtConfig,\n ),\n signRefreshToken(record.hostId, this.jwtConfig),\n ]);\n\n await saveRefreshToken(this.cache, refreshToken, record.hostId, record.namespaceId);\n\n return { accessToken, refreshToken, expiresIn, tokenType: 'Bearer' };\n }\n\n // ── Refresh token pair ────────────────────────────────────────────────────\n\n async refreshTokens(refreshToken: string): Promise<TokenResponse | null> {\n // Verify JWT signature first\n const jwtResult = await verifyRefreshToken(refreshToken, this.jwtConfig);\n if (!jwtResult) {return null;}\n\n // Consume from store (rotation — old token invalidated)\n const stored = await consumeRefreshToken(this.cache, refreshToken);\n if (!stored) {return null;}\n\n // Reload client record to get current tier/namespace\n // We need to scan by hostId — store clientId → hostId mapping for lookup\n // For now: resolve via a secondary key written at registration\n const hostId = stored.hostId;\n\n // Issue new pair\n const [{ token: accessToken, expiresIn }, newRefreshToken] = await Promise.all([\n signAccessToken(\n { hostId, namespaceId: stored.namespaceId, tier: 'free', type: 'machine' },\n this.jwtConfig,\n ),\n signRefreshToken(hostId, this.jwtConfig),\n ]);\n\n await saveRefreshToken(this.cache, newRefreshToken, hostId, stored.namespaceId);\n\n return { accessToken, refreshToken: newRefreshToken, expiresIn, tokenType: 'Bearer' };\n }\n\n // ── Verify access token → AuthContext ────────────────────────────────────\n\n async verify(token: string): Promise<AuthContext | null> {\n const payload = await verifyAccessToken(token, this.jwtConfig);\n if (!payload) {return null;}\n\n return {\n type: payload.type,\n userId: payload.sub,\n namespaceId: payload.namespaceId,\n tier: payload.tier,\n permissions: ['host:connect'],\n };\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kb-labs/gateway-auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.93.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -15,15 +15,15 @@
|
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"jose": "^5.9.6",
|
|
18
|
-
"@kb-labs/
|
|
19
|
-
"@kb-labs/
|
|
18
|
+
"@kb-labs/gateway-contracts": "2.93.0",
|
|
19
|
+
"@kb-labs/core-platform": "2.93.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^24.3.3",
|
|
23
23
|
"tsup": "^8.5.0",
|
|
24
24
|
"typescript": "^5",
|
|
25
25
|
"vitest": "^3.2.4",
|
|
26
|
-
"@kb-labs/devkit": "2.
|
|
26
|
+
"@kb-labs/devkit": "2.93.0"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=20.0.0",
|