@kb-labs/gateway-auth 2.89.0 → 2.94.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 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 validation, token issuance, and request authorization.
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.89.0",
3
+ "version": "2.94.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/core-platform": "2.89.0",
19
- "@kb-labs/gateway-contracts": "2.89.0"
18
+ "@kb-labs/core-platform": "2.94.0",
19
+ "@kb-labs/gateway-contracts": "2.94.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.89.0"
26
+ "@kb-labs/devkit": "2.94.0"
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=20.0.0",