@kb-labs/gateway-auth 0.2.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/dist/index.d.ts +89 -0
- package/dist/index.js +209 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ICache } from '@kb-labs/core-platform';
|
|
2
|
+
import { TokenType, JwtPayload, RegisterRequest, RegisterResponse, TokenResponse, AuthContext } from '@kb-labs/gateway-contracts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JWT sign / verify / refresh logic.
|
|
6
|
+
* Pure functions — no side effects, no ICache dependency.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface JwtConfig {
|
|
10
|
+
/** Raw secret string — converted to TextEncoder internally */
|
|
11
|
+
secret: string;
|
|
12
|
+
}
|
|
13
|
+
interface SignAccessTokenOptions {
|
|
14
|
+
hostId: string;
|
|
15
|
+
namespaceId: string;
|
|
16
|
+
tier: 'free' | 'pro' | 'enterprise';
|
|
17
|
+
type: TokenType;
|
|
18
|
+
}
|
|
19
|
+
declare function signAccessToken(opts: SignAccessTokenOptions, config: JwtConfig): Promise<{
|
|
20
|
+
token: string;
|
|
21
|
+
expiresIn: number;
|
|
22
|
+
}>;
|
|
23
|
+
declare function signRefreshToken(hostId: string, config: JwtConfig): Promise<string>;
|
|
24
|
+
declare function verifyAccessToken(token: string, config: JwtConfig): Promise<JwtPayload | null>;
|
|
25
|
+
declare function verifyRefreshToken(token: string, config: JwtConfig): Promise<{
|
|
26
|
+
hostId: string;
|
|
27
|
+
} | null>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* AuthService — orchestrates registration, token issuance, and refresh.
|
|
31
|
+
* Depends on ICache (via store) and JWT config.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
declare class AuthService {
|
|
35
|
+
private readonly cache;
|
|
36
|
+
private readonly jwtConfig;
|
|
37
|
+
constructor(cache: ICache, jwtConfig: JwtConfig);
|
|
38
|
+
register(req: RegisterRequest): Promise<RegisterResponse & {
|
|
39
|
+
clientSecret: string;
|
|
40
|
+
}>;
|
|
41
|
+
issueTokens(clientId: string, clientSecret: string): Promise<TokenResponse | null>;
|
|
42
|
+
refreshTokens(refreshToken: string): Promise<TokenResponse | null>;
|
|
43
|
+
verify(token: string): Promise<AuthContext | null>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Auth storage — ICache operations for credentials and tokens.
|
|
48
|
+
*
|
|
49
|
+
* Key schema:
|
|
50
|
+
* auth:client:{clientId} → ClientRecord (permanent)
|
|
51
|
+
* auth:refresh:{tokenHash} → { hostId } (TTL 30d)
|
|
52
|
+
* auth:publickey:{hostId} → string (base64url X25519 public key)
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
interface ClientRecord {
|
|
56
|
+
clientId: string;
|
|
57
|
+
/** bcrypt-style: we store sha256 hash of the secret for simplicity (no bcrypt dep) */
|
|
58
|
+
secretHash: string;
|
|
59
|
+
hostId: string;
|
|
60
|
+
namespaceId: string;
|
|
61
|
+
tier: 'free' | 'pro' | 'enterprise';
|
|
62
|
+
name: string;
|
|
63
|
+
capabilities: string[];
|
|
64
|
+
publicKey?: string;
|
|
65
|
+
createdAt: number;
|
|
66
|
+
}
|
|
67
|
+
declare function generateClientId(): string;
|
|
68
|
+
declare function generateClientSecret(): string;
|
|
69
|
+
declare function generateHostId(): string;
|
|
70
|
+
declare function saveClient(cache: ICache, record: ClientRecord): Promise<void>;
|
|
71
|
+
declare function getClientByHostId(cache: ICache, hostId: string): Promise<ClientRecord | null>;
|
|
72
|
+
declare function getClient(cache: ICache, clientId: string): Promise<ClientRecord | null>;
|
|
73
|
+
declare function verifyClientSecret(cache: ICache, clientId: string, secret: string): Promise<ClientRecord | null>;
|
|
74
|
+
declare function buildClientRecord(opts: {
|
|
75
|
+
name: string;
|
|
76
|
+
namespaceId: string;
|
|
77
|
+
capabilities: string[];
|
|
78
|
+
publicKey?: string;
|
|
79
|
+
secret: string;
|
|
80
|
+
}): ClientRecord;
|
|
81
|
+
declare function saveRefreshToken(cache: ICache, token: string, hostId: string, namespaceId: string): Promise<void>;
|
|
82
|
+
declare function consumeRefreshToken(cache: ICache, token: string): Promise<{
|
|
83
|
+
hostId: string;
|
|
84
|
+
namespaceId: string;
|
|
85
|
+
} | null>;
|
|
86
|
+
declare function savePublicKey(cache: ICache, hostId: string, publicKey: string): Promise<void>;
|
|
87
|
+
declare function getPublicKey(cache: ICache, hostId: string): Promise<string | null>;
|
|
88
|
+
|
|
89
|
+
export { AuthService, type ClientRecord, type JwtConfig, buildClientRecord, consumeRefreshToken, generateClientId, generateClientSecret, generateHostId, getClient, getClientByHostId, getPublicKey, saveClient, savePublicKey, saveRefreshToken, signAccessToken, signRefreshToken, verifyAccessToken, verifyClientSecret, verifyRefreshToken };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { randomBytes, randomUUID, createHash } from 'crypto';
|
|
2
|
+
import { SignJWT, jwtVerify } from 'jose';
|
|
3
|
+
|
|
4
|
+
// src/store.ts
|
|
5
|
+
var REFRESH_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
6
|
+
function hashSecret(secret) {
|
|
7
|
+
return createHash("sha256").update(secret).digest("hex");
|
|
8
|
+
}
|
|
9
|
+
function hashToken(token) {
|
|
10
|
+
return createHash("sha256").update(token).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
function generateClientId() {
|
|
13
|
+
return `clt_${randomBytes(16).toString("hex")}`;
|
|
14
|
+
}
|
|
15
|
+
function generateClientSecret() {
|
|
16
|
+
return `cs_${randomBytes(32).toString("base64url")}`;
|
|
17
|
+
}
|
|
18
|
+
function generateHostId() {
|
|
19
|
+
return `host_${randomBytes(12).toString("hex")}`;
|
|
20
|
+
}
|
|
21
|
+
async function saveClient(cache, record) {
|
|
22
|
+
await cache.set(`auth:client:${record.clientId}`, record);
|
|
23
|
+
await cache.set(`auth:hostindex:${record.hostId}`, record.clientId);
|
|
24
|
+
}
|
|
25
|
+
async function getClientByHostId(cache, hostId) {
|
|
26
|
+
const clientId = await cache.get(`auth:hostindex:${hostId}`);
|
|
27
|
+
if (!clientId) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return getClient(cache, clientId);
|
|
31
|
+
}
|
|
32
|
+
async function getClient(cache, clientId) {
|
|
33
|
+
return cache.get(`auth:client:${clientId}`);
|
|
34
|
+
}
|
|
35
|
+
async function verifyClientSecret(cache, clientId, secret) {
|
|
36
|
+
const record = await getClient(cache, clientId);
|
|
37
|
+
if (!record) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (record.secretHash !== hashSecret(secret)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return record;
|
|
44
|
+
}
|
|
45
|
+
function buildClientRecord(opts) {
|
|
46
|
+
return {
|
|
47
|
+
clientId: generateClientId(),
|
|
48
|
+
secretHash: hashSecret(opts.secret),
|
|
49
|
+
hostId: generateHostId(),
|
|
50
|
+
namespaceId: opts.namespaceId,
|
|
51
|
+
tier: "free",
|
|
52
|
+
name: opts.name,
|
|
53
|
+
capabilities: opts.capabilities,
|
|
54
|
+
publicKey: opts.publicKey,
|
|
55
|
+
createdAt: Date.now()
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function saveRefreshToken(cache, token, hostId, namespaceId) {
|
|
59
|
+
await cache.set(`auth:refresh:${hashToken(token)}`, { hostId, namespaceId }, REFRESH_TTL_MS);
|
|
60
|
+
}
|
|
61
|
+
async function consumeRefreshToken(cache, token) {
|
|
62
|
+
const key = `auth:refresh:${hashToken(token)}`;
|
|
63
|
+
const entry = await cache.get(key);
|
|
64
|
+
if (!entry) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
await cache.delete(key);
|
|
68
|
+
return entry;
|
|
69
|
+
}
|
|
70
|
+
async function savePublicKey(cache, hostId, publicKey) {
|
|
71
|
+
await cache.set(`auth:publickey:${hostId}`, publicKey);
|
|
72
|
+
}
|
|
73
|
+
async function getPublicKey(cache, hostId) {
|
|
74
|
+
return cache.get(`auth:publickey:${hostId}`);
|
|
75
|
+
}
|
|
76
|
+
var ACCESS_TOKEN_TTL = 15 * 60;
|
|
77
|
+
var REFRESH_TOKEN_TTL = 30 * 24 * 3600;
|
|
78
|
+
function getSecretKey(secret) {
|
|
79
|
+
return new TextEncoder().encode(secret);
|
|
80
|
+
}
|
|
81
|
+
async function signAccessToken(opts, config) {
|
|
82
|
+
const key = getSecretKey(config.secret);
|
|
83
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
84
|
+
const token = await new SignJWT({
|
|
85
|
+
namespaceId: opts.namespaceId,
|
|
86
|
+
tier: opts.tier,
|
|
87
|
+
type: opts.type
|
|
88
|
+
}).setProtectedHeader({ alg: "HS256" }).setSubject(opts.hostId).setIssuedAt(now).setExpirationTime(now + ACCESS_TOKEN_TTL).sign(key);
|
|
89
|
+
return { token, expiresIn: ACCESS_TOKEN_TTL };
|
|
90
|
+
}
|
|
91
|
+
async function signRefreshToken(hostId, config) {
|
|
92
|
+
const key = getSecretKey(config.secret);
|
|
93
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
94
|
+
const jti = randomUUID();
|
|
95
|
+
return new SignJWT({ type: "refresh", jti }).setProtectedHeader({ alg: "HS256" }).setSubject(hostId).setIssuedAt(now).setExpirationTime(now + REFRESH_TOKEN_TTL).sign(key);
|
|
96
|
+
}
|
|
97
|
+
async function verifyAccessToken(token, config) {
|
|
98
|
+
try {
|
|
99
|
+
const key = getSecretKey(config.secret);
|
|
100
|
+
const { payload } = await jwtVerify(token, key);
|
|
101
|
+
return payload;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function verifyRefreshToken(token, config) {
|
|
107
|
+
try {
|
|
108
|
+
const key = getSecretKey(config.secret);
|
|
109
|
+
const { payload } = await jwtVerify(token, key);
|
|
110
|
+
if (payload.type !== "refresh") {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (!payload.sub) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return { hostId: payload.sub };
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/service.ts
|
|
123
|
+
var AuthService = class {
|
|
124
|
+
constructor(cache, jwtConfig) {
|
|
125
|
+
this.cache = cache;
|
|
126
|
+
this.jwtConfig = jwtConfig;
|
|
127
|
+
}
|
|
128
|
+
// ── Register ──────────────────────────────────────────────────────────────
|
|
129
|
+
async register(req) {
|
|
130
|
+
const secret = generateClientSecret();
|
|
131
|
+
const record = buildClientRecord({
|
|
132
|
+
name: req.name,
|
|
133
|
+
namespaceId: req.namespaceId,
|
|
134
|
+
capabilities: req.capabilities ?? [],
|
|
135
|
+
publicKey: req.publicKey,
|
|
136
|
+
secret
|
|
137
|
+
});
|
|
138
|
+
await saveClient(this.cache, record);
|
|
139
|
+
if (req.publicKey) {
|
|
140
|
+
await savePublicKey(this.cache, record.hostId, req.publicKey);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
clientId: record.clientId,
|
|
144
|
+
clientSecret: secret,
|
|
145
|
+
// returned ONCE, never stored in plaintext
|
|
146
|
+
hostId: record.hostId
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// ── Issue token pair ──────────────────────────────────────────────────────
|
|
150
|
+
async issueTokens(clientId, clientSecret) {
|
|
151
|
+
const record = await verifyClientSecret(this.cache, clientId, clientSecret);
|
|
152
|
+
if (!record) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const [{ token: accessToken, expiresIn }, refreshToken] = await Promise.all([
|
|
156
|
+
signAccessToken(
|
|
157
|
+
{
|
|
158
|
+
hostId: record.hostId,
|
|
159
|
+
namespaceId: record.namespaceId,
|
|
160
|
+
tier: record.tier,
|
|
161
|
+
type: "machine"
|
|
162
|
+
},
|
|
163
|
+
this.jwtConfig
|
|
164
|
+
),
|
|
165
|
+
signRefreshToken(record.hostId, this.jwtConfig)
|
|
166
|
+
]);
|
|
167
|
+
await saveRefreshToken(this.cache, refreshToken, record.hostId, record.namespaceId);
|
|
168
|
+
return { accessToken, refreshToken, expiresIn, tokenType: "Bearer" };
|
|
169
|
+
}
|
|
170
|
+
// ── Refresh token pair ────────────────────────────────────────────────────
|
|
171
|
+
async refreshTokens(refreshToken) {
|
|
172
|
+
const jwtResult = await verifyRefreshToken(refreshToken, this.jwtConfig);
|
|
173
|
+
if (!jwtResult) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const stored = await consumeRefreshToken(this.cache, refreshToken);
|
|
177
|
+
if (!stored) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const hostId = stored.hostId;
|
|
181
|
+
const [{ token: accessToken, expiresIn }, newRefreshToken] = await Promise.all([
|
|
182
|
+
signAccessToken(
|
|
183
|
+
{ hostId, namespaceId: stored.namespaceId, tier: "free", type: "machine" },
|
|
184
|
+
this.jwtConfig
|
|
185
|
+
),
|
|
186
|
+
signRefreshToken(hostId, this.jwtConfig)
|
|
187
|
+
]);
|
|
188
|
+
await saveRefreshToken(this.cache, newRefreshToken, hostId, stored.namespaceId);
|
|
189
|
+
return { accessToken, refreshToken: newRefreshToken, expiresIn, tokenType: "Bearer" };
|
|
190
|
+
}
|
|
191
|
+
// ── Verify access token → AuthContext ────────────────────────────────────
|
|
192
|
+
async verify(token) {
|
|
193
|
+
const payload = await verifyAccessToken(token, this.jwtConfig);
|
|
194
|
+
if (!payload) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
type: payload.type,
|
|
199
|
+
userId: payload.sub,
|
|
200
|
+
namespaceId: payload.namespaceId,
|
|
201
|
+
tier: payload.tier,
|
|
202
|
+
permissions: ["host:connect"]
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export { AuthService, buildClientRecord, consumeRefreshToken, generateClientId, generateClientSecret, generateHostId, getClient, getClientByHostId, getPublicKey, saveClient, savePublicKey, saveRefreshToken, signAccessToken, signRefreshToken, verifyAccessToken, verifyClientSecret, verifyRefreshToken };
|
|
208
|
+
//# sourceMappingURL=index.js.map
|
|
209
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kb-labs/gateway-auth",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"scripts": {
|
|
18
|
+
"clean": "rimraf dist",
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"type-check": "tsc --noEmit",
|
|
22
|
+
"lint": "eslint .",
|
|
23
|
+
"lint:fix": "eslint . --fix",
|
|
24
|
+
"test": "vitest run -c ../../vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest -c ../../vitest.config.ts"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@kb-labs/core-platform": "^1.5.0",
|
|
29
|
+
"@kb-labs/gateway-contracts": "^0.2.0",
|
|
30
|
+
"jose": "^5.9.6"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@kb-labs/devkit": "link:../../../kb-labs-devkit",
|
|
34
|
+
"@types/node": "^20",
|
|
35
|
+
"tsup": "^8.5.0",
|
|
36
|
+
"vitest": "^3.2.4"
|
|
37
|
+
}
|
|
38
|
+
}
|