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.
- package/dist/api/authorizer.d.ts +12 -0
- package/dist/api/authorizer.d.ts.map +1 -0
- package/dist/api/authorizer.js +157 -0
- package/dist/api/authorizer.js.map +1 -0
- package/dist/api/config/intent-config.d.ts +45 -0
- package/dist/api/config/intent-config.d.ts.map +1 -0
- package/dist/api/config/intent-config.js +95 -0
- package/dist/api/config/intent-config.js.map +1 -0
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +722 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/routing/intent-router.d.ts +56 -0
- package/dist/api/routing/intent-router.d.ts.map +1 -0
- package/dist/api/routing/intent-router.js +140 -0
- package/dist/api/routing/intent-router.js.map +1 -0
- package/dist/api/routing/intent-validator.d.ts +83 -0
- package/dist/api/routing/intent-validator.d.ts.map +1 -0
- package/dist/api/routing/intent-validator.js +187 -0
- package/dist/api/routing/intent-validator.js.map +1 -0
- package/dist/api/services/billing.d.ts +9 -0
- package/dist/api/services/billing.d.ts.map +1 -0
- package/dist/api/services/billing.js +49 -0
- package/dist/api/services/billing.js.map +1 -0
- package/dist/api/services/stripe.d.ts +17 -0
- package/dist/api/services/stripe.d.ts.map +1 -0
- package/dist/api/services/stripe.js +69 -0
- package/dist/api/services/stripe.js.map +1 -0
- package/dist/authorizer.zip +0 -0
- package/dist/cli/auth.d.ts +20 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +264 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/config.d.ts +17 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +94 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +371 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/proxy.d.ts +2 -0
- package/dist/cli/proxy.d.ts.map +1 -0
- package/dist/cli/proxy.js +171 -0
- package/dist/cli/proxy.js.map +1 -0
- package/dist/data/catalog.d.ts +54 -0
- package/dist/data/catalog.d.ts.map +1 -0
- package/dist/data/catalog.js +108 -0
- package/dist/data/catalog.js.map +1 -0
- package/dist/db/dal.d.ts +75 -0
- package/dist/db/dal.d.ts.map +1 -0
- package/dist/db/dal.js +124 -0
- package/dist/db/dal.js.map +1 -0
- package/dist/index.js +156 -0
- package/dist/index.ts +134 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +19 -0
- package/dist/logger.js.map +1 -0
- package/dist/test/setup.d.ts +2 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/setup.js +27 -0
- package/dist/test/setup.js.map +1 -0
- 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
|