zeroauth 1.0.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.
Files changed (64) hide show
  1. package/dist/api/authorizer.d.ts +12 -0
  2. package/dist/api/authorizer.d.ts.map +1 -0
  3. package/dist/api/authorizer.js +157 -0
  4. package/dist/api/authorizer.js.map +1 -0
  5. package/dist/api/config/intent-config.d.ts +45 -0
  6. package/dist/api/config/intent-config.d.ts.map +1 -0
  7. package/dist/api/config/intent-config.js +95 -0
  8. package/dist/api/config/intent-config.js.map +1 -0
  9. package/dist/api/index.d.ts +4 -0
  10. package/dist/api/index.d.ts.map +1 -0
  11. package/dist/api/index.js +722 -0
  12. package/dist/api/index.js.map +1 -0
  13. package/dist/api/routing/intent-router.d.ts +56 -0
  14. package/dist/api/routing/intent-router.d.ts.map +1 -0
  15. package/dist/api/routing/intent-router.js +140 -0
  16. package/dist/api/routing/intent-router.js.map +1 -0
  17. package/dist/api/routing/intent-validator.d.ts +83 -0
  18. package/dist/api/routing/intent-validator.d.ts.map +1 -0
  19. package/dist/api/routing/intent-validator.js +187 -0
  20. package/dist/api/routing/intent-validator.js.map +1 -0
  21. package/dist/api/services/billing.d.ts +9 -0
  22. package/dist/api/services/billing.d.ts.map +1 -0
  23. package/dist/api/services/billing.js +49 -0
  24. package/dist/api/services/billing.js.map +1 -0
  25. package/dist/api/services/stripe.d.ts +17 -0
  26. package/dist/api/services/stripe.d.ts.map +1 -0
  27. package/dist/api/services/stripe.js +69 -0
  28. package/dist/api/services/stripe.js.map +1 -0
  29. package/dist/authorizer.zip +0 -0
  30. package/dist/cli/auth.d.ts +20 -0
  31. package/dist/cli/auth.d.ts.map +1 -0
  32. package/dist/cli/auth.js +264 -0
  33. package/dist/cli/auth.js.map +1 -0
  34. package/dist/cli/config.d.ts +17 -0
  35. package/dist/cli/config.d.ts.map +1 -0
  36. package/dist/cli/config.js +94 -0
  37. package/dist/cli/config.js.map +1 -0
  38. package/dist/cli/index.d.ts +3 -0
  39. package/dist/cli/index.d.ts.map +1 -0
  40. package/dist/cli/index.js +371 -0
  41. package/dist/cli/index.js.map +1 -0
  42. package/dist/cli/proxy.d.ts +2 -0
  43. package/dist/cli/proxy.d.ts.map +1 -0
  44. package/dist/cli/proxy.js +171 -0
  45. package/dist/cli/proxy.js.map +1 -0
  46. package/dist/data/catalog.d.ts +54 -0
  47. package/dist/data/catalog.d.ts.map +1 -0
  48. package/dist/data/catalog.js +108 -0
  49. package/dist/data/catalog.js.map +1 -0
  50. package/dist/db/dal.d.ts +75 -0
  51. package/dist/db/dal.d.ts.map +1 -0
  52. package/dist/db/dal.js +124 -0
  53. package/dist/db/dal.js.map +1 -0
  54. package/dist/index.js +156 -0
  55. package/dist/index.ts +134 -0
  56. package/dist/logger.d.ts +8 -0
  57. package/dist/logger.d.ts.map +1 -0
  58. package/dist/logger.js +19 -0
  59. package/dist/logger.js.map +1 -0
  60. package/dist/test/setup.d.ts +2 -0
  61. package/dist/test/setup.d.ts.map +1 -0
  62. package/dist/test/setup.js +27 -0
  63. package/dist/test/setup.js.map +1 -0
  64. package/package.json +82 -0
@@ -0,0 +1,722 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.validateEnvironmentVariables = validateEnvironmentVariables;
40
+ const logger_1 = require("../logger");
41
+ const dal_1 = require("../db/dal");
42
+ const express_1 = __importDefault(require("express"));
43
+ const cors_1 = __importDefault(require("cors"));
44
+ const axios_1 = __importDefault(require("axios"));
45
+ const jwks_rsa_1 = __importDefault(require("jwks-rsa"));
46
+ const jwt = __importStar(require("jsonwebtoken"));
47
+ const catalog_1 = require("../data/catalog");
48
+ const billing_1 = require("./services/billing");
49
+ logger_1.log.info("=== ZeroAuth API starting ===");
50
+ logger_1.log.info("Environment:", process.env.NODE_ENV);
51
+ logger_1.log.info("AWS Lambda function name:", process.env.AWS_LAMBDA_FUNCTION_NAME);
52
+ logger_1.log.info("Port:", process.env.PORT || 8080);
53
+ const app = (0, express_1.default)();
54
+ const PORT = process.env.PORT || 8080;
55
+ app.use((0, cors_1.default)());
56
+ app.use(express_1.default.json({ limit: "50mb" }));
57
+ app.use(express_1.default.urlencoded({ limit: "50mb", extended: true }));
58
+ // Initialize curated catalog for routing
59
+ try {
60
+ catalog_1.catalogLoader.loadCatalog();
61
+ logger_1.log.info("✅ Curated catalog loaded successfully");
62
+ }
63
+ catch (error) {
64
+ logger_1.log.error("❌ Failed to load curated catalog:", error);
65
+ }
66
+ // --- ADD THIS AUTHENTICATION MIDDLEWARE ---
67
+ // Keep constants for use throughout the module
68
+ const jwksUri = process.env.OIDC_JWKS_URL;
69
+ const issuer = process.env.OIDC_ISSUER_URL;
70
+ const audience = process.env.OIDC_AUDIENCE;
71
+ const internalSecret = process.env.CLOUDFRONT_INTERNAL_SECRET;
72
+ // Extract validation logic for testing - check current env vars each time
73
+ function validateEnvironmentVariables() {
74
+ const currentJwksUri = process.env.OIDC_JWKS_URL;
75
+ const currentIssuer = process.env.OIDC_ISSUER_URL;
76
+ const currentAudience = process.env.OIDC_AUDIENCE;
77
+ const currentInternalSecret = process.env.CLOUDFRONT_INTERNAL_SECRET;
78
+ if (!currentJwksUri ||
79
+ !currentIssuer ||
80
+ !currentAudience ||
81
+ !currentInternalSecret) {
82
+ console.error("Missing required OIDC/Secret env vars");
83
+ return false;
84
+ }
85
+ return true;
86
+ }
87
+ // Create a JWKS client
88
+ const jwksClient = (0, jwks_rsa_1.default)({
89
+ jwksUri: jwksUri,
90
+ });
91
+ // Function to get the signing key
92
+ function getKey(header, callback) {
93
+ if (!header.kid) {
94
+ callback(new Error("No KID in JWT header"), undefined);
95
+ return;
96
+ }
97
+ jwksClient.getSigningKey(header.kid, (err, key) => {
98
+ if (err) {
99
+ callback(err, undefined);
100
+ return;
101
+ }
102
+ const signingKey = key.getPublicKey();
103
+ callback(null, signingKey);
104
+ });
105
+ }
106
+ // Middleware 1: Check for the CloudFront secret
107
+ const verifyCloudFrontSecret = (req, res, next) => {
108
+ const secret = req.headers["x-internal-secret"];
109
+ logger_1.log.info("=== CloudFront Secret Check ===");
110
+ logger_1.log.info(`Headers received: ${JSON.stringify(Object.keys(req.headers))}`);
111
+ logger_1.log.info(`x-internal-secret present: ${!!secret}`);
112
+ logger_1.log.info(`Expected secret: ${internalSecret ? "set" : "NOT SET"}`);
113
+ logger_1.log.info(`Host: ${req.get("host")}`);
114
+ logger_1.log.info(`Request path: ${req.path}`);
115
+ // Allow requests with correct secret
116
+ if (secret === internalSecret) {
117
+ logger_1.log.info("✅ CloudFront secret verified - allowing request");
118
+ next();
119
+ return;
120
+ }
121
+ // Allow localhost requests for local development
122
+ const host = req.get("host");
123
+ const isTestEnvironment = process.env.NODE_ENV === "test";
124
+ if (!isTestEnvironment &&
125
+ host &&
126
+ (host.startsWith("localhost") || host.startsWith("127.0.0.1"))) {
127
+ logger_1.log.info("✅ Localhost request - allowing without CloudFront secret");
128
+ next();
129
+ return;
130
+ }
131
+ logger_1.log.warn(`❌ CloudFront secret check failed - received: ${secret}, expected: ${internalSecret}`);
132
+ logger_1.log.warn("Blocking request with missing/invalid secret");
133
+ return res.status(403).json({ error: "Forbidden" });
134
+ };
135
+ // Middleware 2: Check for the Clerk JWT
136
+ const authenticate = (req, res, next) => {
137
+ logger_1.log.info("=== JWT Authentication Check ===");
138
+ logger_1.log.info(`Headers: ${JSON.stringify(Object.keys(req.headers))}`);
139
+ // Check both standard Authorization header and x-authorization (CloudFront workaround)
140
+ const authHeader = (req.headers.authorization ||
141
+ req.headers["x-authorization"]);
142
+ logger_1.log.info(`Auth header found: ${!!authHeader}`);
143
+ logger_1.log.info(`Auth header starts with Bearer: ${authHeader?.startsWith("Bearer ") || false}`);
144
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
145
+ logger_1.log.warn("❌ JWT auth failed - missing or invalid auth header");
146
+ return res.status(401).json({ error: "Unauthorized: Missing token" });
147
+ }
148
+ const token = authHeader.split(" ")[1];
149
+ jwt.verify(token, getKey, {
150
+ issuer: issuer,
151
+ audience: audience,
152
+ }, async (err, decoded) => {
153
+ if (err) {
154
+ console.error("JWT verification failed:", err.message);
155
+ return res.status(401).json({ error: `Unauthorized: ${err.message}` });
156
+ }
157
+ // Identity Resolution for Billing
158
+ try {
159
+ const claims = decoded;
160
+ const clerkUserId = claims.sub;
161
+ if (clerkUserId) {
162
+ const userIdentity = await (0, dal_1.findOrCreateUserIdentity)(clerkUserId);
163
+ const organizationId = userIdentity.securityProfile.organization_id;
164
+ req.customer = organizationId;
165
+ // Ensure customer exists at signup/authentication
166
+ const email = claims.email || claims.preferred_username;
167
+ const name = claims.name;
168
+ // Fire and forget (optional, but ensureCustomer is idempotent and fast if exists)
169
+ billing_1.BillingService.ensureCustomer(organizationId, email, name).catch((err) => logger_1.log.error({ err, organizationId }, "Failed to ensure Stripe customer in middleware"));
170
+ logger_1.log.info(`✅ Resolved Customer ID from User Identity: ${req.customer}`);
171
+ }
172
+ }
173
+ catch (dbError) {
174
+ logger_1.log.error("Failed to resolve user identity from DB:", dbError);
175
+ // We allow the request to proceed for now, but billing might fail later
176
+ }
177
+ next();
178
+ });
179
+ };
180
+ // ... (Rest of middleware) ...
181
+ // --- ADD NEW WORKLOAD IDENTITY MIDDLEWARE ---
182
+ const authenticateWorkloadIdentity = async (req, res, next) => {
183
+ logger_1.log.info("Attempting Workload Identity authentication...");
184
+ // Check both standard Authorization header and x-authorization (CloudFront workaround)
185
+ const authHeader = (req.headers.authorization ||
186
+ req.headers["x-authorization"]);
187
+ const host = req.get("host"); // Get the host header
188
+ if (!authHeader) {
189
+ logger_1.log.warn("Workload Identity auth failed: Missing Authorization or x-authorization header");
190
+ return res.status(401).json({ error: "Unauthorized: Missing credentials" });
191
+ }
192
+ try {
193
+ let customerId = null;
194
+ // Route auth logic based on host header
195
+ if (host === "aws.iam.zeroauth.ai") {
196
+ // <-- Check for the new CNAME
197
+ // --- AWS SigV4 Auth ---
198
+ if (!authHeader.startsWith("AWS4-HMAC-SHA256")) {
199
+ logger_1.log.warn("AWS host detected but auth header is not SigV4.");
200
+ return res.status(401).json({ error: "Invalid AWS credentials" });
201
+ }
202
+ logger_1.log.info("Detected AWS SigV4 credentials for host aws.iam.zeroauth.ai.");
203
+ // TODO: Implement full SigV4 signature validation here
204
+ // FULL AWS SIGV4 SIGNATURE VALIDATION IMPLEMENTATION
205
+ logger_1.log.info("Performing full AWS SigV4 signature validation...");
206
+ // Parse AWS SigV4 Authorization header components
207
+ const authParts = {
208
+ algorithm: "",
209
+ credential: "",
210
+ signedHeaders: "",
211
+ signature: "",
212
+ };
213
+ // Parse Authorization header components
214
+ logger_1.log.info(`Parsing authorization header: ${authHeader}`);
215
+ const authComponents = authHeader.split(",").map((part) => part.trim());
216
+ logger_1.log.info(`Auth components after split and trim: ${JSON.stringify(authComponents, null, 2)}`);
217
+ for (const component of authComponents) {
218
+ logger_1.log.info(`Processing component: "${component}"`);
219
+ if (component.startsWith("AWS4-HMAC-SHA256")) {
220
+ authParts.algorithm = "AWS4-HMAC-SHA256";
221
+ logger_1.log.info(`Found algorithm: ${authParts.algorithm}`);
222
+ // Extract credential from the first component (AWS4-HMAC-SHA256 Credential=...)
223
+ const credentialMatch = component.match(/Credential=(.+)/);
224
+ if (credentialMatch) {
225
+ authParts.credential = credentialMatch[1];
226
+ logger_1.log.info(`Extracted credential from first component: ${authParts.credential}`);
227
+ }
228
+ }
229
+ else if (component.startsWith("Credential=")) {
230
+ authParts.credential = component.substring(10);
231
+ logger_1.log.info(`Found credential: ${authParts.credential}`);
232
+ }
233
+ else if (component.startsWith("SignedHeaders=")) {
234
+ authParts.signedHeaders = component.substring(14);
235
+ logger_1.log.info(`Found signed headers: ${authParts.signedHeaders}`);
236
+ }
237
+ else if (component.startsWith("Signature=")) {
238
+ authParts.signature = component.substring(10);
239
+ logger_1.log.info(`Found signature: ${authParts.signature}`);
240
+ }
241
+ }
242
+ logger_1.log.info(`Parsed auth parts: ${JSON.stringify(authParts, null, 2)}`);
243
+ if (!authParts.algorithm ||
244
+ !authParts.credential ||
245
+ !authParts.signature) {
246
+ logger_1.log.warn("Missing required AWS SigV4 authorization components");
247
+ return res
248
+ .status(401)
249
+ .json({ error: "Invalid AWS SigV4 authorization header" });
250
+ }
251
+ // Parse credential scope: access_key/date/region/service/aws4_request
252
+ const credentialParts = authParts.credential.split("/");
253
+ if (credentialParts.length < 5) {
254
+ logger_1.log.warn("Invalid AWS SigV4 credential scope");
255
+ return res.status(401).json({ error: "Invalid AWS credential scope" });
256
+ }
257
+ const accessKey = credentialParts[0];
258
+ const dateStamp = credentialParts[1];
259
+ const region = credentialParts[2];
260
+ const service = credentialParts[3];
261
+ logger_1.log.info(`AWS SigV4 - Access Key: ${accessKey}, Region: ${region}, Service: ${service}, Date: ${dateStamp}`);
262
+ // Build canonical request for signature validation
263
+ const httpMethod = req.method;
264
+ const canonicalUri = req.path;
265
+ const canonicalQueryString = ""; // No query parameters for API Gateway
266
+ const canonicalHeaders = buildCanonicalHeaders(req, authParts.signedHeaders);
267
+ const payloadHash = calculatePayloadHash(req.body);
268
+ const canonicalRequest = [
269
+ httpMethod,
270
+ canonicalUri,
271
+ canonicalQueryString,
272
+ canonicalHeaders,
273
+ authParts.signedHeaders,
274
+ payloadHash,
275
+ ].join("\n");
276
+ logger_1.log.info(`Canonical request:\n${canonicalRequest}`);
277
+ // Build string to sign
278
+ const algorithm = "AWS4-HMAC-SHA256";
279
+ const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
280
+ const hashedCanonicalRequest = await sha256Hex(canonicalRequest);
281
+ const stringToSign = [
282
+ algorithm,
283
+ req.headers["x-amz-date"],
284
+ credentialScope,
285
+ hashedCanonicalRequest,
286
+ ].join("\n");
287
+ logger_1.log.info(`String to sign:\n${stringToSign}`);
288
+ // Use AWS SDK to validate credentials and get caller identity
289
+ try {
290
+ // Use AWS SDK to validate the credentials by calling STS
291
+ const AWS = require("aws-sdk");
292
+ // Configure AWS with the provided credentials
293
+ AWS.config.update({
294
+ accessKeyId: accessKey,
295
+ region: region,
296
+ });
297
+ // Handle session token for STS temporary credentials
298
+ const sessionToken = req.headers["x-amz-security-token"];
299
+ if (sessionToken) {
300
+ AWS.config.update({
301
+ sessionToken: sessionToken,
302
+ });
303
+ }
304
+ const sts = new AWS.STS();
305
+ // Get caller identity to validate the credentials and get actual ARN
306
+ const callerIdentity = await sts.getCallerIdentity().promise();
307
+ logger_1.log.info(`✅ AWS STS GetCallerIdentity successful for ARN: ${callerIdentity.Arn}`);
308
+ // Use the actual ARN from STS for database lookup
309
+ const actualArn = callerIdentity.Arn;
310
+ // Database lookup using the actual AWS ARN
311
+ const workloadIdentity = await (0, dal_1.findWorkloadIdentity)("AWS", actualArn);
312
+ if (workloadIdentity) {
313
+ customerId = workloadIdentity.securityProfile.organization_id;
314
+ logger_1.log.info(`✅ Successfully validated AWS SigV4 and found workload identity for ARN: ${actualArn}`);
315
+ }
316
+ else {
317
+ logger_1.log.warn(`⚠️ AWS SigV4 valid but no workload identity found in database for ARN: ${actualArn}`);
318
+ }
319
+ }
320
+ catch (error) {
321
+ logger_1.log.error(`AWS STS validation failed: ${error instanceof Error ? error.message : String(error)}`);
322
+ return res.status(401).json({ error: "Invalid AWS credentials" });
323
+ }
324
+ // Helper functions for AWS SigV4 validation
325
+ function buildCanonicalHeaders(req, signedHeaders) {
326
+ const headers = req.headers;
327
+ const canonicalHeaders = [];
328
+ const signedHeaderList = signedHeaders.split(";");
329
+ for (const headerName of signedHeaderList) {
330
+ const headerValue = headers[headerName.toLowerCase()] || "";
331
+ const cleanHeaderName = headerName.toLowerCase().trim();
332
+ const cleanHeaderValue = String(headerValue)
333
+ .replace(/\s+/g, " ")
334
+ .trim();
335
+ canonicalHeaders.push(`${cleanHeaderName}:${cleanHeaderValue}`);
336
+ }
337
+ return canonicalHeaders.join("\n");
338
+ }
339
+ function calculatePayloadHash(body) {
340
+ const payload = JSON.stringify(body);
341
+ return require("crypto")
342
+ .createHash("sha256")
343
+ .update(payload)
344
+ .digest("hex");
345
+ }
346
+ function sha256Hex(data) {
347
+ return Promise.resolve(require("crypto").createHash("sha256").update(data).digest("hex"));
348
+ }
349
+ }
350
+ else if (host === "gcp.iam.zeroauth.ai" ||
351
+ host === "azure.iam.zeroauth.ai") {
352
+ //
353
+ // --- GCP/Azure OIDC Auth ---
354
+ if (!authHeader.startsWith("Bearer ")) {
355
+ logger_1.log.warn("GCP/Azure host detected but auth header is not Bearer token.");
356
+ return res.status(401).json({ error: "Invalid OIDC token" });
357
+ }
358
+ logger_1.log.info(`Detected Bearer token for OIDC host: ${host}.`);
359
+ const token = authHeader.split(" ")[1];
360
+ // STUBBED LOGIC: Decode token to find claims
361
+ const claims = jwt.decode(token);
362
+ let identity = null;
363
+ if (claims &&
364
+ claims.iss &&
365
+ claims.iss.includes("accounts.google.com") &&
366
+ host === "gcp.iam.zeroauth.ai") {
367
+ identity = claims.sub; // Or relevant claim for Project ID
368
+ logger_1.log.info(`GCP OIDC validation stub: Using identity: ${identity}`);
369
+ // Database lookup for GCP workload identity
370
+ if (identity) {
371
+ const workloadIdentity = await (0, dal_1.findWorkloadIdentity)("GCP", identity);
372
+ if (workloadIdentity) {
373
+ const organizationId = workloadIdentity.securityProfile.organization_id;
374
+ customerId = organizationId;
375
+ // Ensure customer exists at "signup" (first workload discovery)
376
+ billing_1.BillingService.ensureCustomer(organizationId).catch((err) => logger_1.log.error({ err, organizationId }, "Failed to ensure Stripe customer for workload"));
377
+ logger_1.log.info(`Found GCP workload identity in database for: ${identity}`);
378
+ }
379
+ }
380
+ }
381
+ else if (claims &&
382
+ claims.iss &&
383
+ claims.iss.includes("sts.windows.net") &&
384
+ host === "azure.iam.zeroauth.ai") {
385
+ identity = claims.tid; // Tenant ID
386
+ logger_1.log.info(`Azure OIDC validation stub: Using identity: ${identity}`);
387
+ // Database lookup for Azure workload identity
388
+ if (identity) {
389
+ const workloadIdentity = await (0, dal_1.findWorkloadIdentity)("AZURE", identity);
390
+ if (workloadIdentity) {
391
+ customerId = workloadIdentity.securityProfile.organization_id;
392
+ logger_1.log.info(`Found Azure workload identity in database for: ${identity}`);
393
+ }
394
+ }
395
+ }
396
+ }
397
+ else {
398
+ // Host does not match any known workload identity CNAME
399
+ logger_1.log.warn(`Workload Identity auth failed: Host header '${host}' does not match a known identity gateway.`);
400
+ return res.status(403).json({ error: "Forbidden: Invalid host" });
401
+ }
402
+ // --- Authorization Check ---
403
+ if (customerId) {
404
+ logger_1.log.info(`Workload Identity validated. Customer: ${customerId}`);
405
+ req.customer = customerId; // Attach customer to request
406
+ next(); //
407
+ }
408
+ else {
409
+ logger_1.log.warn("Workload Identity authentication failed: Identity not found in database.");
410
+ return res.status(403).json({ error: "Forbidden: Unknown identity" });
411
+ }
412
+ }
413
+ catch (error) {
414
+ logger_1.log.error("Error during Workload Identity authentication:", error.message);
415
+ return res.status(401).json({ error: `Unauthorized: ${error.message}` });
416
+ }
417
+ };
418
+ // --- END NEW MIDDLEWARE ---
419
+ // --- UPDATED: Point to LiteLLM Proxy service ---
420
+ const LITELLM_PROXY_URL = process.env.LITELLM_PROXY_URL ||
421
+ "http://zeroauth-prod-litellm.513e62d22d8a40c69d66ec687a54a0dd.svc.cluster.local";
422
+ // Waitlist endpoint (no authentication required)
423
+ app.post("/waitlist", async (req, res) => {
424
+ logger_1.log.info("=== Processing POST /waitlist ===");
425
+ try {
426
+ const { email } = req.body;
427
+ if (!email) {
428
+ return res.status(400).json({ error: "Email is required" });
429
+ }
430
+ // Basic email validation
431
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
432
+ if (!emailRegex.test(email)) {
433
+ return res.status(400).json({ error: "Invalid email format" });
434
+ }
435
+ // Import Prisma client
436
+ const { PrismaClient } = require("@prisma/client");
437
+ const prisma = new PrismaClient();
438
+ try {
439
+ // Try to create the waitlist entry
440
+ await prisma.waitlistEntry.create({
441
+ data: {
442
+ email: email.toLowerCase().trim(),
443
+ },
444
+ });
445
+ logger_1.log.info(`Added email to waitlist: ${email}`);
446
+ res
447
+ .status(200)
448
+ .json({ success: true, message: "Email added to waitlist" });
449
+ }
450
+ catch (dbError) {
451
+ // Handle unique constraint violation (email already exists)
452
+ if (dbError.code === "P2002") {
453
+ logger_1.log.info(`Email already exists in waitlist: ${email}`);
454
+ return res
455
+ .status(200)
456
+ .json({ success: true, message: "You're already on our waitlist!" });
457
+ }
458
+ // Other database errors
459
+ logger_1.log.error("Database error:", dbError);
460
+ throw dbError;
461
+ }
462
+ finally {
463
+ await prisma.$disconnect();
464
+ }
465
+ }
466
+ catch (error) {
467
+ logger_1.log.error("Error in waitlist endpoint:", error);
468
+ res.status(500).json({ error: "Internal server error" });
469
+ }
470
+ });
471
+ // Apply the secret checker to API routes (but not to waitlist)
472
+ app.use(verifyCloudFrontSecret);
473
+ // Routes
474
+ app.get("/v1/models", async (req, res) => {
475
+ logger_1.log.info("=== Processing GET /v1/models ===");
476
+ try {
477
+ logger_1.log.info("Forwarding request to:", `${LITELLM_PROXY_URL}/models`);
478
+ const response = await axios_1.default.get(`${LITELLM_PROXY_URL}/models`, {
479
+ headers: {
480
+ Authorization: "Bearer sk-zeroauth-dev", // Use master key from proxy-config.yaml
481
+ },
482
+ timeout: 30000,
483
+ });
484
+ logger_1.log.info("Response received from LiteLLM:", response.status, response.statusText);
485
+ logger_1.log.info("Response data:", JSON.stringify(response.data, null, 2));
486
+ res.json(response.data);
487
+ }
488
+ catch (error) {
489
+ console.error("Error fetching models:", error.message);
490
+ res.status(500).json({ error: "Could not fetch model list." });
491
+ }
492
+ });
493
+ // Middleware: Set default Free Developer Profile (no compliance checks)
494
+ const setDefaultProfile = (req, res, next) => {
495
+ // Hardcode the Free Developer Profile for all requests (PoC)
496
+ req.complianceProfile = catalog_1.catalogLoader.getDefaultProfile();
497
+ logger_1.log.info("📋 Applied default Free Developer Profile (no compliance checks)");
498
+ next();
499
+ };
500
+ // This handler has all your proxy logic
501
+ const chatCompletionsHandler = async (req, res) => {
502
+ logger_1.log.info(`=== Processing ${req.path} ===`);
503
+ logger_1.log.info("Request body:", JSON.stringify(req.body, null, 2));
504
+ logger_1.log.info("Model:", req.body.model);
505
+ logger_1.log.info("Stream:", req.body.stream);
506
+ logger_1.log.info("All body keys:", Object.keys(req.body));
507
+ logger_1.log.info("LITELLM_PROXY_URL:", LITELLM_PROXY_URL);
508
+ logger_1.log.info("LITELLM_PROXY_URL:", LITELLM_PROXY_URL);
509
+ logger_1.log.info("Checking if LITELLM_PROXY_URL is reachable...");
510
+ // Add health check before making the request
511
+ try {
512
+ const healthCheck = await axios_1.default.get(`${LITELLM_PROXY_URL}/health`, {
513
+ headers: {
514
+ Authorization: "Bearer sk-zeroauth-dev",
515
+ },
516
+ timeout: 5000,
517
+ });
518
+ logger_1.log.info("LiteLLM health check response:", healthCheck.status);
519
+ }
520
+ catch (healthError) {
521
+ logger_1.log.error("LiteLLM health check failed:", healthError.message);
522
+ logger_1.log.error("Health check error details:", healthError.code, healthError.response?.status);
523
+ }
524
+ logger_1.log.info("Proceeding with chat completions request...");
525
+ // Billing Hook: Ensure customer exists (Lazy check)
526
+ const customerId = req.customer;
527
+ if (!customerId) {
528
+ logger_1.log.warn("⚠️ No Customer ID found in request. Billing usage will NOT be recorded.");
529
+ }
530
+ try {
531
+ const requestBody = { ...req.body };
532
+ const originalModel = requestBody.model;
533
+ logger_1.log.info("Original request body copied");
534
+ logger_1.log.info("Original model parameter:", originalModel);
535
+ // Implement static file-based intent routing
536
+ let routingType = "direct";
537
+ let intentAlias = null;
538
+ let targetModel = originalModel;
539
+ // Check for intent: prefix
540
+ if (originalModel &&
541
+ typeof originalModel === "string" &&
542
+ originalModel.startsWith("intent:")) {
543
+ const intent = originalModel.substring(7); // Remove "intent:" prefix
544
+ logger_1.log.info(`🔍 Detected intent request: ${intent}`);
545
+ // Look up intent in curated catalog
546
+ const catalogEntry = catalog_1.catalogLoader.findModelByIntent(`intent:${intent}`);
547
+ if (catalogEntry) {
548
+ targetModel = catalogEntry.model_name;
549
+ intentAlias = `intent:${intent}`;
550
+ routingType = "intent_to_group";
551
+ logger_1.log.info(`✅ Intent resolved: ${intentAlias} → ${targetModel}`);
552
+ }
553
+ else {
554
+ // Invalid intent - return helpful error
555
+ const availableIntents = catalog_1.catalogLoader.getAvailableIntents();
556
+ logger_1.log.warn(`❌ Invalid intent: ${intent}`);
557
+ return res.status(400).json({
558
+ error: {
559
+ message: `Invalid intent: "${intent}". Available intents: ${availableIntents.join(", ")}`,
560
+ type: "invalid_request_error",
561
+ code: "invalid_intent",
562
+ zeroauth_help: {
563
+ available_intents: availableIntents,
564
+ example: 'Use model: "intent:budget" for cost-effective models',
565
+ },
566
+ },
567
+ });
568
+ }
569
+ }
570
+ else {
571
+ // Direct model name - pass through unchanged
572
+ logger_1.log.info("📋 Direct model request - no routing needed");
573
+ }
574
+ requestBody.model = targetModel;
575
+ logger_1.log.info("🎯 Routing decision:", {
576
+ original: originalModel,
577
+ target: targetModel,
578
+ routingType,
579
+ intent: intentAlias,
580
+ });
581
+ logger_1.log.info("Final transformed request body:", JSON.stringify(requestBody, null, 2));
582
+ logger_1.log.info("Making request to:", `${LITELLM_PROXY_URL}/chat/completions`);
583
+ if (requestBody.stream) {
584
+ logger_1.log.info("Handling streaming request");
585
+ const response = await axios_1.default.post(`${LITELLM_PROXY_URL}/chat/completions`, requestBody, {
586
+ headers: {
587
+ "Content-Type": "application/json",
588
+ Authorization: "Bearer sk-zeroauth-dev", // Use master key from proxy-config.yaml
589
+ },
590
+ responseType: "stream",
591
+ timeout: 300000, // Increase timeout for potentially long AI responses
592
+ });
593
+ logger_1.log.info("Stream response received, status:", response.status);
594
+ logger_1.log.info("Stream response headers:", JSON.stringify(response.headers, null, 2));
595
+ res.setHeader("Content-Type", "text/event-stream");
596
+ res.setHeader("Cache-Control", "no-cache");
597
+ res.setHeader("Connection", "keep-alive");
598
+ response.data.pipe(res);
599
+ response.data.on("error", (err) => {
600
+ logger_1.log.error("Stream error:", err);
601
+ if (!res.headersSent) {
602
+ res.status(500).json({ error: "Stream error" });
603
+ }
604
+ });
605
+ response.data.on("end", () => {
606
+ logger_1.log.info("Stream ended");
607
+ res.end();
608
+ });
609
+ }
610
+ else {
611
+ logger_1.log.info("Handling non-streaming request");
612
+ const response = await axios_1.default.post(`${LITELLM_PROXY_URL}/chat/completions`, requestBody, {
613
+ headers: {
614
+ "Content-Type": "application/json",
615
+ Authorization: "Bearer sk-zeroauth-dev", // Use master key from proxy-config.yaml
616
+ },
617
+ timeout: 300000, // Increase timeout for potentially long AI responses
618
+ });
619
+ logger_1.log.info("Non-streaming response received, status:", response.status);
620
+ logger_1.log.info("Non-streaming response headers:", JSON.stringify(response.headers, null, 2));
621
+ logger_1.log.info("Response data:", JSON.stringify(response.data, null, 2));
622
+ // Add ZeroAuth routing metadata to response
623
+ const responseData = response.data;
624
+ // BILLING HOOK: Record Usage
625
+ if (responseData.usage && customerId) {
626
+ const totalTokens = responseData.usage.total_tokens || 0;
627
+ logger_1.log.info({ customerId, totalTokens }, "Recording usage for billing");
628
+ // Fire and forget (don't await to reduce latency)
629
+ billing_1.BillingService.recordUsage(customerId, totalTokens).catch(err => logger_1.log.error({ err }, "Background usage recording failed"));
630
+ }
631
+ if (routingType === "intent_to_group") {
632
+ responseData.zeroauth = {
633
+ routed: true,
634
+ original_intent: intentAlias,
635
+ target_model_group: targetModel,
636
+ routing_type: routingType,
637
+ };
638
+ }
639
+ res.json(responseData);
640
+ }
641
+ }
642
+ catch (error) {
643
+ logger_1.log.error(`=== Error in ${req.path} ===`);
644
+ logger_1.log.error("Error message:", error.message);
645
+ logger_1.log.error("Error code:", error.code);
646
+ logger_1.log.error("Error response status:", error.response?.status);
647
+ logger_1.log.error("Error response headers:", JSON.stringify(error.response?.headers, null, 2));
648
+ logger_1.log.error("Error response data:", JSON.stringify(error.response?.data, null, 2));
649
+ logger_1.log.error("Request config:", JSON.stringify(error.config, null, 2));
650
+ logger_1.log.error("Full error:", error);
651
+ // Parse the actual error message from LiteLLM response
652
+ let actualErrorMessage = error.message;
653
+ if (error.response?.data?.error) {
654
+ const errorData = error.response.data.error;
655
+ if (typeof errorData === "object" && errorData.message) {
656
+ if (typeof errorData.message === "string" &&
657
+ errorData.message.startsWith("{'error':")) {
658
+ try {
659
+ const match = errorData.message.match(/'error':\s*'([^']+)'/);
660
+ if (match) {
661
+ actualErrorMessage = match[1];
662
+ }
663
+ else {
664
+ actualErrorMessage = errorData.message;
665
+ }
666
+ }
667
+ catch {
668
+ actualErrorMessage = errorData.message;
669
+ }
670
+ }
671
+ else {
672
+ actualErrorMessage = errorData.message;
673
+ }
674
+ }
675
+ else if (typeof errorData === "string") {
676
+ actualErrorMessage = errorData;
677
+ }
678
+ }
679
+ logger_1.log.error("Parsed error message:", actualErrorMessage);
680
+ logger_1.log.error("Full error:", error);
681
+ // Propagate the actual LiteLLM error details
682
+ const status = error.response?.status || 500;
683
+ const errorData = error.response?.data;
684
+ if (errorData && typeof errorData === "object") {
685
+ // If LiteLLM returns structured error data, pass it through
686
+ res.status(status).json(errorData);
687
+ }
688
+ else {
689
+ // Fallback to generic error with details
690
+ res.status(status).json({
691
+ error: errorData || "Error forwarding request to LiteLLM proxy",
692
+ details: error.message,
693
+ });
694
+ }
695
+ }
696
+ };
697
+ // Developer (JWT) Route
698
+ app.post("/v1/chat/completions", authenticate, // This one checks the JWT
699
+ setDefaultProfile, // Set Free Developer Profile
700
+ chatCompletionsHandler);
701
+ // Production (IAM) Route
702
+ app.post("/v1/iam/chat/completions", authenticateWorkloadIdentity, // <-- ADDED: Validate SigV4/OIDC token based on Host header
703
+ setDefaultProfile, // Set Free Developer Profile
704
+ chatCompletionsHandler);
705
+ // Start server
706
+ // In Lambda environment, always start the server for Lambda Web Adapter
707
+ logger_1.log.info("Checking if we should start the server...");
708
+ logger_1.log.info("require.main === module:", require.main === module);
709
+ logger_1.log.info("process.env.AWS_LAMBDA_FUNCTION_NAME:", process.env.AWS_LAMBDA_FUNCTION_NAME);
710
+ if (require.main === module || process.env.AWS_LAMBDA_FUNCTION_NAME) {
711
+ logger_1.log.info(`Starting server on port ${PORT}...`);
712
+ app.listen(PORT, () => {
713
+ logger_1.log.info(`ZeroAuth API server running on port ${PORT}.`);
714
+ });
715
+ }
716
+ else {
717
+ logger_1.log.info("Not starting server - not main module and not in Lambda environment");
718
+ // Check environment variables on startup using current values
719
+ validateEnvironmentVariables();
720
+ }
721
+ exports.default = app;
722
+ //# sourceMappingURL=index.js.map