@layer-ai/core 0.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/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/lib/db/postgres.d.ts +27 -0
- package/dist/lib/db/postgres.d.ts.map +1 -0
- package/dist/lib/db/postgres.js +168 -0
- package/dist/lib/db/redis.d.ts +12 -0
- package/dist/lib/db/redis.d.ts.map +1 -0
- package/dist/lib/db/redis.js +95 -0
- package/dist/middleware/auth.d.ts +22 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +89 -0
- package/dist/routes/auth.d.ts +4 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +82 -0
- package/dist/routes/complete.d.ts +4 -0
- package/dist/routes/complete.d.ts.map +1 -0
- package/dist/routes/complete.js +223 -0
- package/dist/routes/gates.d.ts +4 -0
- package/dist/routes/gates.d.ts.map +1 -0
- package/dist/routes/gates.js +262 -0
- package/dist/routes/keys.d.ts +4 -0
- package/dist/routes/keys.d.ts.map +1 -0
- package/dist/routes/keys.js +70 -0
- package/dist/routes/logs.d.ts +4 -0
- package/dist/routes/logs.d.ts.map +1 -0
- package/dist/routes/logs.js +120 -0
- package/dist/services/providers/anthropic.d.ts +18 -0
- package/dist/services/providers/anthropic.d.ts.map +1 -0
- package/dist/services/providers/anthropic.js +55 -0
- package/dist/services/providers/base-adapter.d.ts +32 -0
- package/dist/services/providers/base-adapter.d.ts.map +1 -0
- package/dist/services/providers/base-adapter.js +89 -0
- package/dist/services/providers/google.d.ts +18 -0
- package/dist/services/providers/google.d.ts.map +1 -0
- package/dist/services/providers/google.js +39 -0
- package/dist/services/providers/openai-adapter.d.ts +19 -0
- package/dist/services/providers/openai-adapter.d.ts.map +1 -0
- package/dist/services/providers/openai-adapter.js +240 -0
- package/dist/services/providers/openai.d.ts +17 -0
- package/dist/services/providers/openai.d.ts.map +1 -0
- package/dist/services/providers/openai.js +43 -0
- package/dist/services/providers/test-openai-adapter.d.ts +2 -0
- package/dist/services/providers/test-openai-adapter.d.ts.map +1 -0
- package/dist/services/providers/test-openai-adapter.js +118 -0
- package/dist/services/task-analysis.d.ts +7 -0
- package/dist/services/task-analysis.d.ts.map +1 -0
- package/dist/services/task-analysis.js +74 -0
- package/package.json +53 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { default as authRouter } from './routes/auth.js';
|
|
2
|
+
export { default as gatesRouter } from './routes/gates.js';
|
|
3
|
+
export { default as keysRouter } from './routes/keys.js';
|
|
4
|
+
export { default as logsRouter } from './routes/logs.js';
|
|
5
|
+
export { default as completeRouter } from './routes/complete.js';
|
|
6
|
+
export { authenticate } from './middleware/auth.js';
|
|
7
|
+
export { db } from './lib/db/postgres.js';
|
|
8
|
+
export { default as redis } from './lib/db/redis.js';
|
|
9
|
+
export { createCompletion as createOpenAICompletion } from './services/providers/openai.js';
|
|
10
|
+
export { createCompletion as createAnthropicCompletion } from './services/providers/anthropic.js';
|
|
11
|
+
export { createCompletion as createGoogleCompletion } from './services/providers/google.js';
|
|
12
|
+
export type { ProviderResponse } from './services/providers/openai.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGjE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGpD,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAC;AAC1C,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,gBAAgB,IAAI,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAC5F,OAAO,EAAE,gBAAgB,IAAI,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAClG,OAAO,EAAE,gBAAgB,IAAI,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAC5F,YAAY,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Routes
|
|
2
|
+
export { default as authRouter } from './routes/auth.js';
|
|
3
|
+
export { default as gatesRouter } from './routes/gates.js';
|
|
4
|
+
export { default as keysRouter } from './routes/keys.js';
|
|
5
|
+
export { default as logsRouter } from './routes/logs.js';
|
|
6
|
+
export { default as completeRouter } from './routes/complete.js';
|
|
7
|
+
// Middleware
|
|
8
|
+
export { authenticate } from './middleware/auth.js';
|
|
9
|
+
// Database
|
|
10
|
+
export { db } from './lib/db/postgres.js';
|
|
11
|
+
export { default as redis } from './lib/db/redis.js';
|
|
12
|
+
// Services - only export specific items to avoid conflicts
|
|
13
|
+
export { createCompletion as createOpenAICompletion } from './services/providers/openai.js';
|
|
14
|
+
export { createCompletion as createAnthropicCompletion } from './services/providers/anthropic.js';
|
|
15
|
+
export { createCompletion as createGoogleCompletion } from './services/providers/google.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import type { User, ApiKey, Gate } from '@layer-ai/sdk';
|
|
3
|
+
declare function getPool(): pg.Pool;
|
|
4
|
+
export declare const db: {
|
|
5
|
+
query(text: string, params?: any[]): Promise<pg.QueryResult<any>>;
|
|
6
|
+
getUserByEmail(email: string): Promise<User | null>;
|
|
7
|
+
getUserById(id: string): Promise<User | null>;
|
|
8
|
+
createUser(email: string, passwordHash: string): Promise<User>;
|
|
9
|
+
getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
|
|
10
|
+
createApiKey(userId: string, keyHash: string, keyPrefix: string, name: string): Promise<ApiKey>;
|
|
11
|
+
updateApiKeyLastUsed(keyHash: string): Promise<void>;
|
|
12
|
+
getApiKeysForUser(userId: string): Promise<ApiKey[]>;
|
|
13
|
+
deleteApiKey(id: string, userId: string): Promise<boolean>;
|
|
14
|
+
getGateByUserAndName(userId: string, gateName: string): Promise<Gate | null>;
|
|
15
|
+
getGatesForUser(userId: string): Promise<Gate[]>;
|
|
16
|
+
createGate(userId: string, data: any): Promise<Gate>;
|
|
17
|
+
getGateById(id: string): Promise<Gate | null>;
|
|
18
|
+
updateGate(id: string, data: any): Promise<Gate | null>;
|
|
19
|
+
deleteGate(id: string): Promise<boolean>;
|
|
20
|
+
logRequest(data: any): Promise<void>;
|
|
21
|
+
getSessionKeyByHash(keyHash: string): Promise<{
|
|
22
|
+
userId: string;
|
|
23
|
+
expiresAt: Date;
|
|
24
|
+
} | null>;
|
|
25
|
+
};
|
|
26
|
+
export default getPool;
|
|
27
|
+
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../../src/lib/db/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAyB,MAAM,eAAe,CAAC;AAO/E,iBAAS,OAAO,IAAI,EAAE,CAAC,IAAI,CAqB1B;AA0BD,eAAO,MAAM,EAAE;gBAEK,MAAM,WAAW,GAAG,EAAE;0BASZ,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;oBAQnC,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;sBAQ3B,MAAM,gBAAgB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;6BASrC,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;yBAQnC,MAAM,WAAW,MAAM,aAAa,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;kCAQjE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;8BAO1B,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;qBAQnC,MAAM,UAAU,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;iCAS7B,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;4BAQpD,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;uBAQ7B,MAAM,QAAQ,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;oBAsBpC,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;mBAQ9B,MAAM,QAAQ,GAAG,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;mBAgCxC,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;qBASvB,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;iCAgBP,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CAOhG,CAAC;AAEF,eAAe,OAAO,CAAC"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
const { Pool } = pg;
|
|
3
|
+
// Lazy-initialize connection pool
|
|
4
|
+
let pool = null;
|
|
5
|
+
function getPool() {
|
|
6
|
+
if (!pool) {
|
|
7
|
+
pool = new Pool({
|
|
8
|
+
connectionString: process.env.DATABASE_URL,
|
|
9
|
+
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
|
10
|
+
max: 20, // max num of connections
|
|
11
|
+
idleTimeoutMillis: 30000,
|
|
12
|
+
connectionTimeoutMillis: 2000,
|
|
13
|
+
});
|
|
14
|
+
// test connection on startup
|
|
15
|
+
pool.on('connect', () => {
|
|
16
|
+
console.log('Connected to PostgreSQL database');
|
|
17
|
+
});
|
|
18
|
+
pool.on('error', (err) => {
|
|
19
|
+
console.error('Unexpected database error:', err);
|
|
20
|
+
process.exit(-1);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return pool;
|
|
24
|
+
}
|
|
25
|
+
// function to convert snake_case DB cols to camelCase TypeScript
|
|
26
|
+
function toCamelCase(obj) {
|
|
27
|
+
if (!obj)
|
|
28
|
+
return obj;
|
|
29
|
+
const converted = {};
|
|
30
|
+
for (const key in obj) {
|
|
31
|
+
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
32
|
+
let value = obj[key];
|
|
33
|
+
// Convert numeric strings to numbers for specific fields
|
|
34
|
+
if ((camelKey === 'temperature' || camelKey === 'topP') && typeof value === 'string') {
|
|
35
|
+
value = parseFloat(value);
|
|
36
|
+
}
|
|
37
|
+
if (camelKey === 'maxTokens' && typeof value === 'string') {
|
|
38
|
+
value = parseInt(value, 10);
|
|
39
|
+
}
|
|
40
|
+
converted[camelKey] = value;
|
|
41
|
+
}
|
|
42
|
+
return converted;
|
|
43
|
+
}
|
|
44
|
+
// Database query functions
|
|
45
|
+
export const db = {
|
|
46
|
+
// generic query function
|
|
47
|
+
async query(text, params) {
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
const res = await getPool().query(text, params);
|
|
50
|
+
const duration = Date.now() - start;
|
|
51
|
+
console.log('Executed query', { text, duration, rows: res.rowCount });
|
|
52
|
+
return res;
|
|
53
|
+
},
|
|
54
|
+
// Users
|
|
55
|
+
async getUserByEmail(email) {
|
|
56
|
+
const result = await getPool().query('SELECT * FROM users WHERE email = $1', [email]);
|
|
57
|
+
return result.rows[0] ? toCamelCase(result.rows[0]) : null;
|
|
58
|
+
},
|
|
59
|
+
async getUserById(id) {
|
|
60
|
+
const result = await getPool().query('SELECT * FROM users WHERE id = $1', [id]);
|
|
61
|
+
return result.rows[0] ? toCamelCase(result.rows[0]) : null;
|
|
62
|
+
},
|
|
63
|
+
async createUser(email, passwordHash) {
|
|
64
|
+
const result = await getPool().query('INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING *', [email, passwordHash]);
|
|
65
|
+
return toCamelCase(result.rows[0]);
|
|
66
|
+
},
|
|
67
|
+
// API Keys
|
|
68
|
+
async getApiKeyByHash(keyHash) {
|
|
69
|
+
const result = await getPool().query('SELECT * FROM api_keys WHERE key_hash = $1 AND is_active = true', [keyHash]);
|
|
70
|
+
return result.rows[0] ? toCamelCase(result.rows[0]) : null;
|
|
71
|
+
},
|
|
72
|
+
async createApiKey(userId, keyHash, keyPrefix, name) {
|
|
73
|
+
const result = await getPool().query('INSERT INTO api_keys (user_id, key_hash, key_prefix, name) VALUES ($1, $2, $3, $4) RETURNING *', [userId, keyHash, keyPrefix, name]);
|
|
74
|
+
return toCamelCase(result.rows[0]);
|
|
75
|
+
},
|
|
76
|
+
async updateApiKeyLastUsed(keyHash) {
|
|
77
|
+
await getPool().query('UPDATE api_keys SET last_used_at = NOW() WHERE key_hash = $1', [keyHash]);
|
|
78
|
+
},
|
|
79
|
+
async getApiKeysForUser(userId) {
|
|
80
|
+
const result = await getPool().query('SELECT id, user_id, key_prefix, name, created_at, last_used_at FROM api_keys WHERE user_id = $1 AND is_active = true ORDER BY created_at DESC', [userId]);
|
|
81
|
+
return result.rows.map(toCamelCase);
|
|
82
|
+
},
|
|
83
|
+
async deleteApiKey(id, userId) {
|
|
84
|
+
const result = await getPool().query('UPDATE api_keys SET is_active = false WHERE id = $1 AND user_id = $2', [id, userId]);
|
|
85
|
+
return (result.rowCount ?? 0) > 0;
|
|
86
|
+
},
|
|
87
|
+
// Gates
|
|
88
|
+
async getGateByUserAndName(userId, gateName) {
|
|
89
|
+
const result = await getPool().query('SELECT * FROM gates WHERE user_id = $1 AND name = $2', [userId, gateName]);
|
|
90
|
+
return result.rows[0] ? toCamelCase(result.rows[0]) : null;
|
|
91
|
+
},
|
|
92
|
+
async getGatesForUser(userId) {
|
|
93
|
+
const result = await getPool().query('SELECT * FROM gates WHERE user_id = $1 ORDER BY created_at DESC', [userId]);
|
|
94
|
+
return result.rows.map(toCamelCase);
|
|
95
|
+
},
|
|
96
|
+
async createGate(userId, data) {
|
|
97
|
+
const result = await getPool().query(`INSERT INTO gates (user_id, name, description, model, system_prompt, allow_overrides, temperature, max_tokens, top_p, tags, routing_strategy, fallback_models)
|
|
98
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`, [
|
|
99
|
+
userId,
|
|
100
|
+
data.name,
|
|
101
|
+
data.description,
|
|
102
|
+
data.model,
|
|
103
|
+
data.systemPrompt,
|
|
104
|
+
data.allowOverrides ? JSON.stringify(data.allowOverrides) : null,
|
|
105
|
+
data.temperature,
|
|
106
|
+
data.maxTokens,
|
|
107
|
+
data.topP,
|
|
108
|
+
JSON.stringify(data.tags || []),
|
|
109
|
+
data.routingStrategy || 'single',
|
|
110
|
+
JSON.stringify(data.fallbackModels || [])
|
|
111
|
+
]);
|
|
112
|
+
return toCamelCase(result.rows[0]);
|
|
113
|
+
},
|
|
114
|
+
async getGateById(id) {
|
|
115
|
+
const result = await getPool().query('SELECT * FROM gates WHERE id = $1', [id]);
|
|
116
|
+
return result.rows[0] ? toCamelCase(result.rows[0]) : null;
|
|
117
|
+
},
|
|
118
|
+
async updateGate(id, data) {
|
|
119
|
+
const result = await getPool().query(`UPDATE gates SET
|
|
120
|
+
description = COALESCE($2, description),
|
|
121
|
+
model = COALESCE($3, model),
|
|
122
|
+
system_prompt = COALESCE($4, system_prompt),
|
|
123
|
+
allow_overrides = COALESCE($5, allow_overrides),
|
|
124
|
+
temperature = COALESCE($6, temperature),
|
|
125
|
+
max_tokens = COALESCE($7, max_tokens),
|
|
126
|
+
top_p = COALESCE($8, top_p),
|
|
127
|
+
tags = COALESCE($9, tags),
|
|
128
|
+
routing_strategy = COALESCE($10, routing_strategy),
|
|
129
|
+
fallback_models = COALESCE($11, fallback_models),
|
|
130
|
+
updated_at = NOW()
|
|
131
|
+
WHERE id = $1 RETURNING *`, [
|
|
132
|
+
id,
|
|
133
|
+
data.description,
|
|
134
|
+
data.model,
|
|
135
|
+
data.systemPrompt,
|
|
136
|
+
data.allowOverrides ? JSON.stringify(data.allowOverrides) : null,
|
|
137
|
+
data.temperature,
|
|
138
|
+
data.maxTokens,
|
|
139
|
+
data.topP,
|
|
140
|
+
data.tags ? JSON.stringify(data.tags) : null,
|
|
141
|
+
data.routingStrategy,
|
|
142
|
+
data.fallbackModels ? JSON.stringify(data.fallbackModels) : null,
|
|
143
|
+
]);
|
|
144
|
+
return result.rows[0] ? toCamelCase(result.rows[0]) : null;
|
|
145
|
+
},
|
|
146
|
+
async deleteGate(id) {
|
|
147
|
+
const result = await getPool().query('DELETE FROM gates WHERE id = $1', [id]);
|
|
148
|
+
return (result.rowCount ?? 0) > 0;
|
|
149
|
+
},
|
|
150
|
+
// Request Logging
|
|
151
|
+
async logRequest(data) {
|
|
152
|
+
await getPool().query(`INSERT INTO requests (
|
|
153
|
+
user_id, gate_id, gate_name, model_requested, model_used, prompt_tokens,
|
|
154
|
+
completion_tokens, total_tokens, cost_usd, latency_ms, success,
|
|
155
|
+
error_message, user_agent, ip_address)
|
|
156
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`, [
|
|
157
|
+
data.userId, data.gateId, data.gateName, data.modelRequested, data.modelUsed, data.promptTokens,
|
|
158
|
+
data.completionTokens, data.totalTokens, data.costUsd, data.latencyMs, data.success,
|
|
159
|
+
data.errorMessage, data.userAgent, data.ipAddress
|
|
160
|
+
]);
|
|
161
|
+
},
|
|
162
|
+
// Session Keys
|
|
163
|
+
async getSessionKeyByHash(keyHash) {
|
|
164
|
+
const result = await getPool().query('SELECT user_id, expires_at FROM session_keys WHERE key_hash = $1 AND expires_at > NOW()', [keyHash]);
|
|
165
|
+
return result.rows[0] ? toCamelCase(result.rows[0]) : null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
export default getPool;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
import type { Gate } from '@layer-ai/sdk';
|
|
3
|
+
declare const redis: Redis;
|
|
4
|
+
export declare const cache: {
|
|
5
|
+
getGate(userId: string, gateName: string): Promise<Gate | null>;
|
|
6
|
+
setGate(userId: string, gateName: string, gate: Gate): Promise<void>;
|
|
7
|
+
invalidateGate(userId: string, gateName: string): Promise<void>;
|
|
8
|
+
invalidateUserGates(userId: string): Promise<void>;
|
|
9
|
+
ping(): Promise<boolean>;
|
|
10
|
+
};
|
|
11
|
+
export default redis;
|
|
12
|
+
//# sourceMappingURL=redis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../../src/lib/db/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAG1C,QAAA,MAAM,KAAK,OAcT,CAAC;AAmBH,eAAO,MAAM,KAAK;oBAEM,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;oBAqB/C,MAAM,YAAY,MAAM,QAAQ,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;2BAW7C,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gCAUnC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAc1C,OAAO,CAAC,OAAO,CAAC;CAQ/B,CAAC;AAEF,eAAe,KAAK,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
// Create redis client
|
|
3
|
+
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
|
|
4
|
+
maxRetriesPerRequest: 3,
|
|
5
|
+
retryStrategy(times) {
|
|
6
|
+
const delay = Math.min(times * 50, 2000);
|
|
7
|
+
return delay;
|
|
8
|
+
},
|
|
9
|
+
reconnectOnError(err) {
|
|
10
|
+
const targetError = 'READONLY';
|
|
11
|
+
if (err.message.includes(targetError)) {
|
|
12
|
+
// Reconnect when redis is in readonly mode
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
// Connection event handlers
|
|
19
|
+
redis.on('connect', () => {
|
|
20
|
+
console.log('Connected to Redis cache');
|
|
21
|
+
});
|
|
22
|
+
redis.on('error', (err) => {
|
|
23
|
+
console.error('Redis connection error:', err);
|
|
24
|
+
});
|
|
25
|
+
// Cache key builders
|
|
26
|
+
const CACHE_TTL = 300;
|
|
27
|
+
function getGateCacheKey(userId, gateName) {
|
|
28
|
+
return `gate:${userId}:${gateName}`;
|
|
29
|
+
}
|
|
30
|
+
// Cache operations
|
|
31
|
+
export const cache = {
|
|
32
|
+
// get the gate
|
|
33
|
+
async getGate(userId, gateName) {
|
|
34
|
+
try {
|
|
35
|
+
const key = getGateCacheKey(userId, gateName);
|
|
36
|
+
const cached = await redis.get(key);
|
|
37
|
+
if (!cached) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const gate = JSON.parse(cached);
|
|
41
|
+
gate.createdAt = new Date(gate.createdAt);
|
|
42
|
+
gate.updatedAt = new Date(gate.updatedAt);
|
|
43
|
+
return gate;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error('Redis get error:', error);
|
|
47
|
+
return null; // if we fail, then we fetch from the db
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
// Set gate in cache
|
|
51
|
+
async setGate(userId, gateName, gate) {
|
|
52
|
+
try {
|
|
53
|
+
const key = getGateCacheKey(userId, gateName);
|
|
54
|
+
await redis.setex(key, CACHE_TTL, JSON.stringify(gate));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('Redis set error:', error);
|
|
58
|
+
// cache miss here is okay
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
// Invalidate gate cache
|
|
62
|
+
async invalidateGate(userId, gateName) {
|
|
63
|
+
try {
|
|
64
|
+
const key = getGateCacheKey(userId, gateName);
|
|
65
|
+
await redis.del(key);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error('Redis delete error:', error);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
// Invalidate all gates for a user
|
|
72
|
+
async invalidateUserGates(userId) {
|
|
73
|
+
try {
|
|
74
|
+
const pattern = `gate:${userId}:*`;
|
|
75
|
+
const keys = await redis.keys(pattern);
|
|
76
|
+
if (keys.length > 0) {
|
|
77
|
+
await redis.del(...keys);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error('Redis bulk delete error:', error);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
// health check
|
|
85
|
+
async ping() {
|
|
86
|
+
try {
|
|
87
|
+
const result = await redis.ping();
|
|
88
|
+
return result === 'PONG';
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
export default redis;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
declare global {
|
|
3
|
+
namespace Express {
|
|
4
|
+
interface Request {
|
|
5
|
+
userId?: string;
|
|
6
|
+
apiKeyHash?: string;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Auth middleware for api key validation
|
|
12
|
+
*
|
|
13
|
+
* Expected header format:
|
|
14
|
+
* Authorization: Bearer layer_abc123...
|
|
15
|
+
*/
|
|
16
|
+
export declare function authenticate(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Optional middleware for endpoints that don't require auth
|
|
19
|
+
* like the health check public endpoints etc.
|
|
20
|
+
*/
|
|
21
|
+
export declare function optionalAuth(req: Request, res: Response, next: NextFunction): void;
|
|
22
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,UAAU,CAAC,EAAE,MAAM,CAAC;SACrB;KACF;CACF;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CA8Ef;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CAWN"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { db } from '../lib/db/postgres.js';
|
|
3
|
+
/**
|
|
4
|
+
* Auth middleware for api key validation
|
|
5
|
+
*
|
|
6
|
+
* Expected header format:
|
|
7
|
+
* Authorization: Bearer layer_abc123...
|
|
8
|
+
*/
|
|
9
|
+
export async function authenticate(req, res, next) {
|
|
10
|
+
try {
|
|
11
|
+
// Extract Authorization header
|
|
12
|
+
const authHeader = req.headers.authorization;
|
|
13
|
+
if (!authHeader) {
|
|
14
|
+
res.status(401).json({
|
|
15
|
+
error: 'unauthorized',
|
|
16
|
+
message: 'Missing Authorization header',
|
|
17
|
+
});
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// check format: "Bearer layer_..."
|
|
21
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
22
|
+
res.status(401).json({
|
|
23
|
+
error: 'unauthorized',
|
|
24
|
+
message: 'Invalid Authorization header format. Expected: Bearer <api_key>',
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const apiKey = authHeader.substring(7); // Remove "Bearer "
|
|
29
|
+
if (!apiKey || !apiKey.startsWith('layer_')) {
|
|
30
|
+
res.status(401).json({
|
|
31
|
+
error: 'unauthorized',
|
|
32
|
+
message: 'Invalid API key format. Must start with "layer_"',
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const keyHash = crypto
|
|
37
|
+
.createHash('sha256')
|
|
38
|
+
.update(apiKey)
|
|
39
|
+
.digest('hex');
|
|
40
|
+
const apiKeyRecord = await db.getApiKeyByHash(keyHash);
|
|
41
|
+
if (!apiKeyRecord) {
|
|
42
|
+
// Not an API key (it's potentially a session key)
|
|
43
|
+
const sessionKey = await db.getSessionKeyByHash(keyHash);
|
|
44
|
+
if (!sessionKey) {
|
|
45
|
+
res.status(401).json({ error: 'unauthorized', message: 'Invalid API key' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
req.userId = sessionKey.userId;
|
|
49
|
+
next();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!apiKeyRecord.isActive) {
|
|
53
|
+
res.status(401).json({
|
|
54
|
+
error: 'unauthorized',
|
|
55
|
+
message: 'API key has been revoked',
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Attach userId to request for downstream handlers
|
|
60
|
+
req.userId = apiKeyRecord.userId;
|
|
61
|
+
req.apiKeyHash = keyHash;
|
|
62
|
+
// Update last_used_at timestamp (async, dont await)
|
|
63
|
+
db.updateApiKeyLastUsed(keyHash).catch((err) => {
|
|
64
|
+
console.error('Failed to update API key last_used_at:', err);
|
|
65
|
+
});
|
|
66
|
+
next();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error('Authentication error:', error);
|
|
70
|
+
res.status(500).json({
|
|
71
|
+
error: 'internal_error',
|
|
72
|
+
message: 'Authentication failed'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Optional middleware for endpoints that don't require auth
|
|
78
|
+
* like the health check public endpoints etc.
|
|
79
|
+
*/
|
|
80
|
+
export function optionalAuth(req, res, next) {
|
|
81
|
+
const authHeader = req.headers.authorization;
|
|
82
|
+
if (!authHeader) {
|
|
83
|
+
// No auth header = proceed without userId
|
|
84
|
+
next();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// if auth header exists, validate it
|
|
88
|
+
authenticate(req, res, next);
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAKpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AA4FpC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import bcrypt from 'bcryptjs';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { db } from '../lib/db/postgres.js';
|
|
5
|
+
const router = Router();
|
|
6
|
+
// POST /auth/signup
|
|
7
|
+
router.post('/signup', async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const { email, password } = req.body;
|
|
10
|
+
if (!email || !password) {
|
|
11
|
+
res.status(400).json({ error: 'bad_request', message: 'Email and password required' });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const existing = await db.getUserByEmail(email);
|
|
15
|
+
if (existing) {
|
|
16
|
+
res.status(409).json({ error: 'conflict', message: 'Email already registered' });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const passwordHash = await bcrypt.hash(password, 10);
|
|
20
|
+
const user = await db.createUser(email, passwordHash);
|
|
21
|
+
res.status(201).json({ id: user.id, email: user.email });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error('Signup error:', error);
|
|
25
|
+
res.status(500).json({ error: 'internal_error', message: 'Failed to create account ' });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// POST /auth/login
|
|
29
|
+
router.post('/login', async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const { email, password } = req.body;
|
|
32
|
+
if (!email || !password) {
|
|
33
|
+
res.status(400).json({ error: 'bad_request', message: 'Email and password required' });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const user = await db.getUserByEmail(email);
|
|
37
|
+
if (!user) {
|
|
38
|
+
res.status(401).json({ error: 'unauthorized', message: 'Invalid credentials' });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const valid = await bcrypt.compare(password, user.passwordHash);
|
|
42
|
+
if (!valid) {
|
|
43
|
+
res.status(401).json({ error: 'unauthorized', message: 'Invalid credentials' });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
res.json({ id: user.id, email: user.email });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('Login error', error);
|
|
50
|
+
res.status(500).json({ error: 'internal_error', message: 'Failed to login' });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// POST /auth/token
|
|
54
|
+
router.post('/token', async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const { email, password } = req.body;
|
|
57
|
+
if (!email || !password) {
|
|
58
|
+
res.status(400).json({ error: 'bad_request', message: 'Email and password required' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const user = await db.getUserByEmail(email);
|
|
62
|
+
if (!user) {
|
|
63
|
+
res.status(401).json({ error: 'unauthorized', message: 'Invalid credentials' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const valid = await bcrypt.compare(password, user.passwordHash);
|
|
67
|
+
if (!valid) {
|
|
68
|
+
res.status(401).json({ error: 'unauthorized', message: 'Invalid credentials' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const rawKey = `layer_${crypto.randomBytes(32).toString('hex')}`;
|
|
72
|
+
const keyHash = crypto.createHash('sha256').update(rawKey).digest('hex');
|
|
73
|
+
const keyPrefix = rawKey.substring(0, 12); // "layer_xxxxxx"
|
|
74
|
+
await db.createApiKey(user.id, keyHash, keyPrefix, 'CLI');
|
|
75
|
+
res.status(201).json({ apiKey: rawKey });
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error('api key creation error', error);
|
|
79
|
+
res.status(500).json({ error: 'internal_error', message: 'Failed to create api key' });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"complete.d.ts","sourceRoot":"","sources":["../../src/routes/complete.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,SAAS,CAAC;AAWpD,QAAA,MAAM,MAAM,EAAE,UAAqB,CAAC;AA2QpC,eAAe,MAAM,CAAC"}
|