@bonginkan/maria 4.3.32 → 4.3.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/READY.manifest.json +1 -1
- package/dist/bin/maria.cjs +152 -39
- package/dist/bin/maria.cjs.map +1 -1
- package/dist/cli.cjs +152 -39
- package/dist/cli.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/server/express-server.cjs +1211 -267
- package/dist/server/express-server.js +1211 -267
- package/dist/server-express.cjs +1211 -267
- package/dist/server-express.cjs.map +1 -1
- package/package.json +3 -3
- package/src/slash-commands/READY.manifest.json +1 -1
|
@@ -5158,22 +5158,22 @@ var init_from = __esm({
|
|
|
5158
5158
|
init_file();
|
|
5159
5159
|
init_fetch_blob();
|
|
5160
5160
|
({ stat } = fs.promises);
|
|
5161
|
-
blobFromSync = (
|
|
5162
|
-
blobFrom = (
|
|
5163
|
-
fileFrom = (
|
|
5164
|
-
fileFromSync = (
|
|
5165
|
-
fromBlob = (stat2,
|
|
5166
|
-
path:
|
|
5161
|
+
blobFromSync = (path4, type) => fromBlob(fs.statSync(path4), path4, type);
|
|
5162
|
+
blobFrom = (path4, type) => stat(path4).then((stat2) => fromBlob(stat2, path4, type));
|
|
5163
|
+
fileFrom = (path4, type) => stat(path4).then((stat2) => fromFile(stat2, path4, type));
|
|
5164
|
+
fileFromSync = (path4, type) => fromFile(fs.statSync(path4), path4, type);
|
|
5165
|
+
fromBlob = (stat2, path4, type = "") => new fetch_blob_default([new BlobDataItem({
|
|
5166
|
+
path: path4,
|
|
5167
5167
|
size: stat2.size,
|
|
5168
5168
|
lastModified: stat2.mtimeMs,
|
|
5169
5169
|
start: 0
|
|
5170
5170
|
})], { type });
|
|
5171
|
-
fromFile = (stat2,
|
|
5172
|
-
path:
|
|
5171
|
+
fromFile = (stat2, path4, type = "") => new file_default([new BlobDataItem({
|
|
5172
|
+
path: path4,
|
|
5173
5173
|
size: stat2.size,
|
|
5174
5174
|
lastModified: stat2.mtimeMs,
|
|
5175
5175
|
start: 0
|
|
5176
|
-
})], path.basename(
|
|
5176
|
+
})], path.basename(path4), { type, lastModified: stat2.mtimeMs });
|
|
5177
5177
|
BlobDataItem = class _BlobDataItem {
|
|
5178
5178
|
#path;
|
|
5179
5179
|
#start;
|
|
@@ -6815,6 +6815,45 @@ var init_src = __esm({
|
|
|
6815
6815
|
supportedSchemas = /* @__PURE__ */ new Set(["data:", "http:", "https:"]);
|
|
6816
6816
|
}
|
|
6817
6817
|
});
|
|
6818
|
+
|
|
6819
|
+
// src/services/media-orchestrator/image-post.ts
|
|
6820
|
+
var image_post_exports = {};
|
|
6821
|
+
__export(image_post_exports, {
|
|
6822
|
+
processImageOptional: () => processImageOptional
|
|
6823
|
+
});
|
|
6824
|
+
async function processImageOptional(bytes, format, keepExif, targetSize) {
|
|
6825
|
+
try {
|
|
6826
|
+
const sharp = (await import('sharp')).default;
|
|
6827
|
+
let img = sharp(bytes).toColourspace("srgb");
|
|
6828
|
+
if (targetSize && Number.isFinite(targetSize.width) && Number.isFinite(targetSize.height)) {
|
|
6829
|
+
const width = Math.max(1, Math.floor(targetSize.width));
|
|
6830
|
+
const height = Math.max(1, Math.floor(targetSize.height));
|
|
6831
|
+
img = img.resize(width, height, { fit: "cover" });
|
|
6832
|
+
}
|
|
6833
|
+
if (keepExif) img = img.withMetadata();
|
|
6834
|
+
const qEnv = Number(process.env.MARIA_SHARP_QUALITY || "80");
|
|
6835
|
+
const quality = Number.isFinite(qEnv) ? Math.max(1, Math.min(100, Math.floor(qEnv))) : 80;
|
|
6836
|
+
const alphaMode = String(process.env.MARIA_SHARP_ALPHA || "").toLowerCase();
|
|
6837
|
+
if (format === "jpg" && alphaMode !== "preserve") {
|
|
6838
|
+
img = img.flatten({ background: { r: 255, g: 255, b: 255 } });
|
|
6839
|
+
}
|
|
6840
|
+
switch (format) {
|
|
6841
|
+
case "png":
|
|
6842
|
+
return await img.png().toBuffer();
|
|
6843
|
+
case "webp":
|
|
6844
|
+
return await img.webp({ quality }).toBuffer();
|
|
6845
|
+
case "jpg":
|
|
6846
|
+
return await img.jpeg({ mozjpeg: true, quality }).toBuffer();
|
|
6847
|
+
}
|
|
6848
|
+
return bytes;
|
|
6849
|
+
} catch {
|
|
6850
|
+
return bytes;
|
|
6851
|
+
}
|
|
6852
|
+
}
|
|
6853
|
+
var init_image_post = __esm({
|
|
6854
|
+
"src/services/media-orchestrator/image-post.ts"() {
|
|
6855
|
+
}
|
|
6856
|
+
});
|
|
6818
6857
|
var rateLimitStore = /* @__PURE__ */ new Map();
|
|
6819
6858
|
var RATE_LIMITS = {
|
|
6820
6859
|
"/image:FREE": { windowMs: 3e3, requests: 1 },
|
|
@@ -6844,11 +6883,11 @@ function getRateLimitConfig(endpoint, plan) {
|
|
|
6844
6883
|
const key = `${endpoint}:${plan.toUpperCase()}`;
|
|
6845
6884
|
return RATE_LIMITS[key] || RATE_LIMITS.default;
|
|
6846
6885
|
}
|
|
6847
|
-
function getEndpointCategory(
|
|
6848
|
-
if (
|
|
6849
|
-
if (
|
|
6850
|
-
if (
|
|
6851
|
-
if (
|
|
6886
|
+
function getEndpointCategory(path4) {
|
|
6887
|
+
if (path4.includes("/image")) return "/image";
|
|
6888
|
+
if (path4.includes("/video")) return "/video";
|
|
6889
|
+
if (path4.includes("/code")) return "/code";
|
|
6890
|
+
if (path4.includes("/chat")) return "/chat";
|
|
6852
6891
|
return "default";
|
|
6853
6892
|
}
|
|
6854
6893
|
async function rateLimitMiddleware(req, res, next) {
|
|
@@ -7105,6 +7144,14 @@ var GeminiMediaProvider = class {
|
|
|
7105
7144
|
async generateImage(req) {
|
|
7106
7145
|
const modelName = this.primaryModel;
|
|
7107
7146
|
const promptPreview = String(req.prompt ?? "").replace(/\s+/g, " ").slice(0, 200);
|
|
7147
|
+
const targetMime = (() => {
|
|
7148
|
+
const fmt = (req.format || "png").toLowerCase();
|
|
7149
|
+
if (fmt === "jpg") return "image/jpeg";
|
|
7150
|
+
if (fmt === "jpeg") return "image/jpeg";
|
|
7151
|
+
if (fmt === "png") return "image/png";
|
|
7152
|
+
if (fmt === "webp") return "image/webp";
|
|
7153
|
+
return "image/png";
|
|
7154
|
+
})();
|
|
7108
7155
|
let resp;
|
|
7109
7156
|
try {
|
|
7110
7157
|
resp = await this.ai.models.generateContent({
|
|
@@ -7112,7 +7159,7 @@ var GeminiMediaProvider = class {
|
|
|
7112
7159
|
contents: [{ role: "user", parts: [{ text: String(req.prompt) }] }],
|
|
7113
7160
|
generationConfig: {
|
|
7114
7161
|
responseModalities: ["IMAGE"],
|
|
7115
|
-
responseMimeType:
|
|
7162
|
+
responseMimeType: targetMime
|
|
7116
7163
|
}
|
|
7117
7164
|
});
|
|
7118
7165
|
} catch (err) {
|
|
@@ -7134,7 +7181,7 @@ var GeminiMediaProvider = class {
|
|
|
7134
7181
|
const mime = p?.inlineData?.mimeType || p?.inline_data?.mime_type || p?.inline_data?.mimeType || p?.inlineData?.mime_type;
|
|
7135
7182
|
if (data) {
|
|
7136
7183
|
const buf = Buffer.from(String(data), "base64");
|
|
7137
|
-
if (buf.length > 0) return { bytes: buf, mime: typeof mime === "string" ? mime :
|
|
7184
|
+
if (buf.length > 0) return { bytes: buf, mime: typeof mime === "string" ? mime : targetMime };
|
|
7138
7185
|
}
|
|
7139
7186
|
}
|
|
7140
7187
|
try {
|
|
@@ -7147,7 +7194,7 @@ var GeminiMediaProvider = class {
|
|
|
7147
7194
|
const bytesB64 = img0?.imageBytes || img0?.bytesBase64Encoded;
|
|
7148
7195
|
if (bytesB64) {
|
|
7149
7196
|
const buf = Buffer.from(String(bytesB64), "base64");
|
|
7150
|
-
if (buf.length > 0) return { bytes: buf, mime:
|
|
7197
|
+
if (buf.length > 0) return { bytes: buf, mime: targetMime };
|
|
7151
7198
|
}
|
|
7152
7199
|
} catch {
|
|
7153
7200
|
}
|
|
@@ -7167,148 +7214,6 @@ var GeminiMediaProvider = class {
|
|
|
7167
7214
|
}
|
|
7168
7215
|
};
|
|
7169
7216
|
|
|
7170
|
-
// src/config/plans/free-plan.json
|
|
7171
|
-
var free_plan_default = {
|
|
7172
|
-
id: "free",
|
|
7173
|
-
name: "Free",
|
|
7174
|
-
displayName: "Free Plan",
|
|
7175
|
-
priceUsd: 0,
|
|
7176
|
-
priceJpy: 0,
|
|
7177
|
-
status: "active",
|
|
7178
|
-
features: {
|
|
7179
|
-
priority: "community",
|
|
7180
|
-
support: "community",
|
|
7181
|
-
accessLevel: "basic"
|
|
7182
|
-
},
|
|
7183
|
-
buckets: {
|
|
7184
|
-
req: 100,
|
|
7185
|
-
tokens: 15e4,
|
|
7186
|
-
code: 20,
|
|
7187
|
-
attachment: 5,
|
|
7188
|
-
image: 25,
|
|
7189
|
-
video: 5
|
|
7190
|
-
},
|
|
7191
|
-
models: [
|
|
7192
|
-
"google:gemini-2.5-flash",
|
|
7193
|
-
"google:gemini-2.0-flash"
|
|
7194
|
-
],
|
|
7195
|
-
imageModels: [
|
|
7196
|
-
"google:imagen-4-fast",
|
|
7197
|
-
"google:gemini-2.5-image"
|
|
7198
|
-
],
|
|
7199
|
-
videoModels: [
|
|
7200
|
-
"google:veo-3-fast",
|
|
7201
|
-
"google:veo-2.0-generate-001"
|
|
7202
|
-
],
|
|
7203
|
-
costRules: {
|
|
7204
|
-
longContextTokens: {
|
|
7205
|
-
threshold: 8e3,
|
|
7206
|
-
multiplier: 2,
|
|
7207
|
-
description: "Long context (>8k tokens) consumes 2x"
|
|
7208
|
-
},
|
|
7209
|
-
imageGen: {
|
|
7210
|
-
multiplier: 1,
|
|
7211
|
-
description: "1 image = 1 image bucket consumption"
|
|
7212
|
-
},
|
|
7213
|
-
videoGen: {
|
|
7214
|
-
multiplier: 1,
|
|
7215
|
-
description: "1 video = 1 video bucket consumption"
|
|
7216
|
-
}
|
|
7217
|
-
},
|
|
7218
|
-
limits: {
|
|
7219
|
-
image: {
|
|
7220
|
-
maxSize: "1024x1024",
|
|
7221
|
-
maxWidth: 1024,
|
|
7222
|
-
maxHeight: 1024,
|
|
7223
|
-
maxCountPerCall: 1,
|
|
7224
|
-
formats: ["png", "jpg", "jpeg", "webp", "svg"],
|
|
7225
|
-
monthlyLimit: 25
|
|
7226
|
-
},
|
|
7227
|
-
video: {
|
|
7228
|
-
maxDurationSec: 8,
|
|
7229
|
-
minDurationSec: 6,
|
|
7230
|
-
maxCountPerCall: 1,
|
|
7231
|
-
aspectWhitelist: ["16:9", "9:16", "1:1"],
|
|
7232
|
-
personGeneration: "DONT_ALLOW",
|
|
7233
|
-
monthlyLimit: 5
|
|
7234
|
-
},
|
|
7235
|
-
code: {
|
|
7236
|
-
maxTokensPerRequest: 8e3,
|
|
7237
|
-
maxOutputTokens: 2048,
|
|
7238
|
-
temperature: {
|
|
7239
|
-
min: 0.1,
|
|
7240
|
-
max: 0.2,
|
|
7241
|
-
default: 0.15
|
|
7242
|
-
}
|
|
7243
|
-
},
|
|
7244
|
-
rateLimit: {
|
|
7245
|
-
requestsPerSecond: 0.33,
|
|
7246
|
-
description: "1 request per 3 seconds"
|
|
7247
|
-
}
|
|
7248
|
-
},
|
|
7249
|
-
fileSave: {
|
|
7250
|
-
allowExtensions: [
|
|
7251
|
-
".ts",
|
|
7252
|
-
".tsx",
|
|
7253
|
-
".js",
|
|
7254
|
-
".jsx",
|
|
7255
|
-
".py",
|
|
7256
|
-
".java",
|
|
7257
|
-
".go",
|
|
7258
|
-
".rs",
|
|
7259
|
-
".html",
|
|
7260
|
-
".css",
|
|
7261
|
-
".scss",
|
|
7262
|
-
".json",
|
|
7263
|
-
".yml",
|
|
7264
|
-
".yaml",
|
|
7265
|
-
".sql",
|
|
7266
|
-
".csv",
|
|
7267
|
-
".md",
|
|
7268
|
-
".sh",
|
|
7269
|
-
".bash",
|
|
7270
|
-
".png",
|
|
7271
|
-
".jpg",
|
|
7272
|
-
".jpeg",
|
|
7273
|
-
".webp",
|
|
7274
|
-
".svg"
|
|
7275
|
-
],
|
|
7276
|
-
maxFileSizeMB: 10,
|
|
7277
|
-
naming: {
|
|
7278
|
-
convention: "kebab-case",
|
|
7279
|
-
pattern: "^[a-z0-9-_]+\\.[a-z]+$"
|
|
7280
|
-
},
|
|
7281
|
-
dirs: {
|
|
7282
|
-
default: "outputs",
|
|
7283
|
-
images: "outputs/images",
|
|
7284
|
-
videos: "outputs/videos",
|
|
7285
|
-
code: "outputs/code"
|
|
7286
|
-
}
|
|
7287
|
-
},
|
|
7288
|
-
ui: {
|
|
7289
|
-
badge: "FREE",
|
|
7290
|
-
badgeColor: "#4CAF50",
|
|
7291
|
-
description: "Perfect for getting started with AI development",
|
|
7292
|
-
highlights: [
|
|
7293
|
-
"100 requests/month",
|
|
7294
|
-
"Gemini 2.5 Flash for code",
|
|
7295
|
-
"25 images/month (1024px)",
|
|
7296
|
-
"5 videos/month (8 sec)",
|
|
7297
|
-
"Community support"
|
|
7298
|
-
]
|
|
7299
|
-
},
|
|
7300
|
-
telemetry: {
|
|
7301
|
-
trackingEnabled: true,
|
|
7302
|
-
anonymousId: true,
|
|
7303
|
-
events: [
|
|
7304
|
-
"model_usage",
|
|
7305
|
-
"command_execution",
|
|
7306
|
-
"error_rate",
|
|
7307
|
-
"latency"
|
|
7308
|
-
]
|
|
7309
|
-
}
|
|
7310
|
-
};
|
|
7311
|
-
|
|
7312
7217
|
// src/services/intelligent-model-selector/IMSFacade.ts
|
|
7313
7218
|
init_SecretManagerIntegration();
|
|
7314
7219
|
|
|
@@ -8580,11 +8485,921 @@ I can still help summarize, clarify, or suggest next steps. Try again in a momen
|
|
|
8580
8485
|
}
|
|
8581
8486
|
};
|
|
8582
8487
|
var IMSFacade_default = IMSFacade;
|
|
8488
|
+
var DEFAULT_STATE = {
|
|
8489
|
+
events: {},
|
|
8490
|
+
customers: {},
|
|
8491
|
+
subscriptions: {},
|
|
8492
|
+
invoices: {},
|
|
8493
|
+
entitlements: {},
|
|
8494
|
+
usage: {}
|
|
8495
|
+
};
|
|
8496
|
+
var SubscriptionStore = class {
|
|
8497
|
+
filePath;
|
|
8498
|
+
state = null;
|
|
8499
|
+
loadPromise = null;
|
|
8500
|
+
queue = Promise.resolve();
|
|
8501
|
+
constructor(filePath) {
|
|
8502
|
+
this.filePath = filePath ?? path__namespace.default.resolve(process.cwd(), "data", "subscription-state.json");
|
|
8503
|
+
}
|
|
8504
|
+
async ensureLoaded() {
|
|
8505
|
+
if (this.state) {
|
|
8506
|
+
return;
|
|
8507
|
+
}
|
|
8508
|
+
if (!this.loadPromise) {
|
|
8509
|
+
this.loadPromise = this.loadStateFromDisk();
|
|
8510
|
+
}
|
|
8511
|
+
await this.loadPromise;
|
|
8512
|
+
}
|
|
8513
|
+
async loadStateFromDisk() {
|
|
8514
|
+
try {
|
|
8515
|
+
const raw = await fsp__namespace.default.readFile(this.filePath, "utf8");
|
|
8516
|
+
const parsed = JSON.parse(raw);
|
|
8517
|
+
this.state = {
|
|
8518
|
+
events: parsed.events ?? {},
|
|
8519
|
+
customers: parsed.customers ?? {},
|
|
8520
|
+
subscriptions: parsed.subscriptions ?? {},
|
|
8521
|
+
invoices: parsed.invoices ?? {},
|
|
8522
|
+
entitlements: parsed.entitlements ?? {},
|
|
8523
|
+
usage: parsed.usage ?? {}
|
|
8524
|
+
};
|
|
8525
|
+
} catch (error) {
|
|
8526
|
+
if (error?.code === "ENOENT") {
|
|
8527
|
+
this.state = { ...DEFAULT_STATE };
|
|
8528
|
+
await this.persist();
|
|
8529
|
+
return;
|
|
8530
|
+
}
|
|
8531
|
+
throw error;
|
|
8532
|
+
}
|
|
8533
|
+
}
|
|
8534
|
+
async persist() {
|
|
8535
|
+
if (!this.state) {
|
|
8536
|
+
return;
|
|
8537
|
+
}
|
|
8538
|
+
const directory = path__namespace.default.dirname(this.filePath);
|
|
8539
|
+
await fsp__namespace.default.mkdir(directory, { recursive: true });
|
|
8540
|
+
const payload = JSON.stringify(this.state, null, 2);
|
|
8541
|
+
await fsp__namespace.default.writeFile(this.filePath, payload, "utf8");
|
|
8542
|
+
}
|
|
8543
|
+
runExclusive(task) {
|
|
8544
|
+
const result = this.queue.then(task);
|
|
8545
|
+
this.queue = result.then(() => void 0).catch(() => void 0);
|
|
8546
|
+
return result;
|
|
8547
|
+
}
|
|
8548
|
+
async markEventReceived(event) {
|
|
8549
|
+
return this.runExclusive(async () => {
|
|
8550
|
+
await this.ensureLoaded();
|
|
8551
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8552
|
+
const state = this.state;
|
|
8553
|
+
const existing = state.events[event.id];
|
|
8554
|
+
if (!existing) {
|
|
8555
|
+
state.events[event.id] = {
|
|
8556
|
+
id: event.id,
|
|
8557
|
+
type: event.type,
|
|
8558
|
+
status: "received",
|
|
8559
|
+
receivedAt: now,
|
|
8560
|
+
stripeCreatedAt: event.created ? new Date(event.created * 1e3).toISOString() : void 0
|
|
8561
|
+
};
|
|
8562
|
+
await this.persist();
|
|
8563
|
+
return "new";
|
|
8564
|
+
}
|
|
8565
|
+
if (existing.status === "processed") {
|
|
8566
|
+
return "duplicate";
|
|
8567
|
+
}
|
|
8568
|
+
state.events[event.id] = {
|
|
8569
|
+
...existing,
|
|
8570
|
+
type: event.type,
|
|
8571
|
+
status: existing.status === "failed" ? "received" : existing.status,
|
|
8572
|
+
receivedAt: existing.receivedAt ?? now
|
|
8573
|
+
};
|
|
8574
|
+
await this.persist();
|
|
8575
|
+
return "pending";
|
|
8576
|
+
});
|
|
8577
|
+
}
|
|
8578
|
+
async markEventProcessed(eventId) {
|
|
8579
|
+
await this.runExclusive(async () => {
|
|
8580
|
+
await this.ensureLoaded();
|
|
8581
|
+
const state = this.state;
|
|
8582
|
+
const existing = state.events[eventId];
|
|
8583
|
+
if (!existing) {
|
|
8584
|
+
return;
|
|
8585
|
+
}
|
|
8586
|
+
state.events[eventId] = {
|
|
8587
|
+
...existing,
|
|
8588
|
+
status: "processed",
|
|
8589
|
+
processedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8590
|
+
error: void 0
|
|
8591
|
+
};
|
|
8592
|
+
await this.persist();
|
|
8593
|
+
});
|
|
8594
|
+
}
|
|
8595
|
+
async markEventFailed(eventId, error) {
|
|
8596
|
+
await this.runExclusive(async () => {
|
|
8597
|
+
await this.ensureLoaded();
|
|
8598
|
+
const state = this.state;
|
|
8599
|
+
const existing = state.events[eventId];
|
|
8600
|
+
if (!existing) {
|
|
8601
|
+
return;
|
|
8602
|
+
}
|
|
8603
|
+
state.events[eventId] = {
|
|
8604
|
+
...existing,
|
|
8605
|
+
status: "failed",
|
|
8606
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8607
|
+
};
|
|
8608
|
+
await this.persist();
|
|
8609
|
+
});
|
|
8610
|
+
}
|
|
8611
|
+
async upsertCustomer(record) {
|
|
8612
|
+
return this.runExclusive(async () => {
|
|
8613
|
+
await this.ensureLoaded();
|
|
8614
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8615
|
+
const state = this.state;
|
|
8616
|
+
const existing = state.customers[record.id];
|
|
8617
|
+
const createdAt = existing?.createdAt ?? record.createdAt ?? now;
|
|
8618
|
+
const nextRecord = {
|
|
8619
|
+
id: record.id,
|
|
8620
|
+
uid: record.uid ?? existing?.uid,
|
|
8621
|
+
email: record.email ?? existing?.email,
|
|
8622
|
+
metadata: {
|
|
8623
|
+
...existing?.metadata ?? {},
|
|
8624
|
+
...record.metadata ?? {}
|
|
8625
|
+
},
|
|
8626
|
+
createdAt,
|
|
8627
|
+
updatedAt: record.updatedAt ?? now
|
|
8628
|
+
};
|
|
8629
|
+
state.customers[record.id] = nextRecord;
|
|
8630
|
+
await this.persist();
|
|
8631
|
+
return nextRecord;
|
|
8632
|
+
});
|
|
8633
|
+
}
|
|
8634
|
+
async getCustomer(customerId) {
|
|
8635
|
+
await this.ensureLoaded();
|
|
8636
|
+
const state = this.state;
|
|
8637
|
+
const record = state.customers[customerId];
|
|
8638
|
+
return record ? { ...record, metadata: record.metadata ? { ...record.metadata } : void 0 } : void 0;
|
|
8639
|
+
}
|
|
8640
|
+
async upsertSubscription(record) {
|
|
8641
|
+
return this.runExclusive(async () => {
|
|
8642
|
+
await this.ensureLoaded();
|
|
8643
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8644
|
+
const state = this.state;
|
|
8645
|
+
const existing = state.subscriptions[record.id];
|
|
8646
|
+
const createdAt = existing?.createdAt ?? record.createdAt ?? now;
|
|
8647
|
+
const nextRecord = {
|
|
8648
|
+
id: record.id,
|
|
8649
|
+
customerId: record.customerId,
|
|
8650
|
+
uid: record.uid ?? existing?.uid,
|
|
8651
|
+
status: record.status,
|
|
8652
|
+
planId: record.planId,
|
|
8653
|
+
pendingPlanId: record.pendingPlanId !== void 0 ? record.pendingPlanId : existing?.pendingPlanId ?? null,
|
|
8654
|
+
pendingPlanEffectiveAt: record.pendingPlanEffectiveAt !== void 0 ? record.pendingPlanEffectiveAt : existing?.pendingPlanEffectiveAt ?? null,
|
|
8655
|
+
cancelAt: record.cancelAt ?? null,
|
|
8656
|
+
cancelAtPeriodEnd: record.cancelAtPeriodEnd ?? false,
|
|
8657
|
+
canceledAt: record.canceledAt ?? null,
|
|
8658
|
+
currentPeriodStart: record.currentPeriodStart,
|
|
8659
|
+
currentPeriodEnd: record.currentPeriodEnd,
|
|
8660
|
+
trialEnd: record.trialEnd ?? null,
|
|
8661
|
+
latestInvoiceId: record.latestInvoiceId ?? existing?.latestInvoiceId,
|
|
8662
|
+
latestInvoiceStatus: record.latestInvoiceStatus ?? existing?.latestInvoiceStatus,
|
|
8663
|
+
latestInvoiceAmount: record.latestInvoiceAmount ?? existing?.latestInvoiceAmount,
|
|
8664
|
+
metadata: {
|
|
8665
|
+
...existing?.metadata ?? {},
|
|
8666
|
+
...record.metadata ?? {}
|
|
8667
|
+
},
|
|
8668
|
+
createdAt,
|
|
8669
|
+
updatedAt: record.updatedAt ?? now
|
|
8670
|
+
};
|
|
8671
|
+
state.subscriptions[record.id] = nextRecord;
|
|
8672
|
+
await this.persist();
|
|
8673
|
+
return nextRecord;
|
|
8674
|
+
});
|
|
8675
|
+
}
|
|
8676
|
+
async getSubscription(subscriptionId) {
|
|
8677
|
+
await this.ensureLoaded();
|
|
8678
|
+
const state = this.state;
|
|
8679
|
+
const record = state.subscriptions[subscriptionId];
|
|
8680
|
+
if (!record) {
|
|
8681
|
+
return void 0;
|
|
8682
|
+
}
|
|
8683
|
+
return {
|
|
8684
|
+
...record,
|
|
8685
|
+
metadata: record.metadata ? { ...record.metadata } : void 0
|
|
8686
|
+
};
|
|
8687
|
+
}
|
|
8688
|
+
async findSubscriptionByCustomer(customerId) {
|
|
8689
|
+
await this.ensureLoaded();
|
|
8690
|
+
const state = this.state;
|
|
8691
|
+
return Object.values(state.subscriptions).find((sub) => sub.customerId === customerId);
|
|
8692
|
+
}
|
|
8693
|
+
async recordInvoice(record) {
|
|
8694
|
+
return this.runExclusive(async () => {
|
|
8695
|
+
await this.ensureLoaded();
|
|
8696
|
+
const state = this.state;
|
|
8697
|
+
state.invoices[record.id] = { ...record };
|
|
8698
|
+
await this.persist();
|
|
8699
|
+
return state.invoices[record.id];
|
|
8700
|
+
});
|
|
8701
|
+
}
|
|
8702
|
+
async setEntitlements(uid, planId, features) {
|
|
8703
|
+
return this.runExclusive(async () => {
|
|
8704
|
+
await this.ensureLoaded();
|
|
8705
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8706
|
+
const state = this.state;
|
|
8707
|
+
const record = {
|
|
8708
|
+
uid,
|
|
8709
|
+
planId,
|
|
8710
|
+
features: [...features],
|
|
8711
|
+
updatedAt: now
|
|
8712
|
+
};
|
|
8713
|
+
state.entitlements[uid] = record;
|
|
8714
|
+
await this.persist();
|
|
8715
|
+
return record;
|
|
8716
|
+
});
|
|
8717
|
+
}
|
|
8718
|
+
async setUsage(params) {
|
|
8719
|
+
return this.runExclusive(async () => {
|
|
8720
|
+
await this.ensureLoaded();
|
|
8721
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8722
|
+
const state = this.state;
|
|
8723
|
+
const key = `${params.uid}:${params.periodId}`;
|
|
8724
|
+
const existing = state.usage[key];
|
|
8725
|
+
const used = existing?.used ?? {
|
|
8726
|
+
requests: 0,
|
|
8727
|
+
tokens: 0,
|
|
8728
|
+
code: 0,
|
|
8729
|
+
attachment: 0
|
|
8730
|
+
};
|
|
8731
|
+
const remaining = {
|
|
8732
|
+
requests: Math.max(0, params.limits.requests - used.requests),
|
|
8733
|
+
tokens: Math.max(0, params.limits.tokens - used.tokens),
|
|
8734
|
+
code: Math.max(0, params.limits.code - used.code),
|
|
8735
|
+
attachment: Math.max(0, params.limits.attachment - used.attachment)
|
|
8736
|
+
};
|
|
8737
|
+
const record = {
|
|
8738
|
+
uid: params.uid,
|
|
8739
|
+
planId: params.planId,
|
|
8740
|
+
periodId: params.periodId,
|
|
8741
|
+
limits: { ...params.limits },
|
|
8742
|
+
used,
|
|
8743
|
+
remaining,
|
|
8744
|
+
createdAt: existing?.createdAt ?? now,
|
|
8745
|
+
updatedAt: now
|
|
8746
|
+
};
|
|
8747
|
+
state.usage[key] = record;
|
|
8748
|
+
await this.persist();
|
|
8749
|
+
return record;
|
|
8750
|
+
});
|
|
8751
|
+
}
|
|
8752
|
+
async getUsage(uid, periodId) {
|
|
8753
|
+
await this.ensureLoaded();
|
|
8754
|
+
const key = `${uid}:${periodId}`;
|
|
8755
|
+
const record = this.state.usage[key];
|
|
8756
|
+
if (!record) {
|
|
8757
|
+
return void 0;
|
|
8758
|
+
}
|
|
8759
|
+
return {
|
|
8760
|
+
...record,
|
|
8761
|
+
limits: { ...record.limits },
|
|
8762
|
+
used: { ...record.used },
|
|
8763
|
+
remaining: { ...record.remaining }
|
|
8764
|
+
};
|
|
8765
|
+
}
|
|
8766
|
+
async getEntitlements(uid) {
|
|
8767
|
+
await this.ensureLoaded();
|
|
8768
|
+
const record = this.state.entitlements[uid];
|
|
8769
|
+
if (!record) {
|
|
8770
|
+
return void 0;
|
|
8771
|
+
}
|
|
8772
|
+
return {
|
|
8773
|
+
...record,
|
|
8774
|
+
features: [...record.features]
|
|
8775
|
+
};
|
|
8776
|
+
}
|
|
8777
|
+
async snapshot() {
|
|
8778
|
+
await this.ensureLoaded();
|
|
8779
|
+
const state = this.state;
|
|
8780
|
+
return JSON.parse(JSON.stringify(state));
|
|
8781
|
+
}
|
|
8782
|
+
};
|
|
8783
|
+
var subscription_store_default = SubscriptionStore;
|
|
8784
|
+
var PLAN_PRIORITY = {
|
|
8785
|
+
free: 0,
|
|
8786
|
+
starter: 1,
|
|
8787
|
+
pro: 2,
|
|
8788
|
+
ultra: 3
|
|
8789
|
+
};
|
|
8790
|
+
var PLAN_CONFIG = {
|
|
8791
|
+
free: {
|
|
8792
|
+
entitlements: [
|
|
8793
|
+
"core.cli.basic",
|
|
8794
|
+
"ai.chat.basic",
|
|
8795
|
+
"usage.metrics.basic"
|
|
8796
|
+
],
|
|
8797
|
+
quota: {
|
|
8798
|
+
requests: 100,
|
|
8799
|
+
tokens: 5e4,
|
|
8800
|
+
code: 30,
|
|
8801
|
+
attachment: 10
|
|
8802
|
+
}
|
|
8803
|
+
},
|
|
8804
|
+
starter: {
|
|
8805
|
+
entitlements: [
|
|
8806
|
+
"core.cli.basic",
|
|
8807
|
+
"ai.chat.basic",
|
|
8808
|
+
"ai.chat.priority",
|
|
8809
|
+
"usage.metrics.expanded",
|
|
8810
|
+
"workspace.multi-device"
|
|
8811
|
+
],
|
|
8812
|
+
quota: {
|
|
8813
|
+
requests: 500,
|
|
8814
|
+
tokens: 25e4,
|
|
8815
|
+
code: 200,
|
|
8816
|
+
attachment: 50
|
|
8817
|
+
}
|
|
8818
|
+
},
|
|
8819
|
+
pro: {
|
|
8820
|
+
entitlements: [
|
|
8821
|
+
"core.cli.basic",
|
|
8822
|
+
"ai.chat.priority",
|
|
8823
|
+
"ai.code.advanced",
|
|
8824
|
+
"orchestrator.code",
|
|
8825
|
+
"usage.metrics.expanded",
|
|
8826
|
+
"workspace.multi-device",
|
|
8827
|
+
"api.webhooks"
|
|
8828
|
+
],
|
|
8829
|
+
quota: {
|
|
8830
|
+
requests: 2e3,
|
|
8831
|
+
tokens: 1e6,
|
|
8832
|
+
code: 1e3,
|
|
8833
|
+
attachment: 200
|
|
8834
|
+
}
|
|
8835
|
+
},
|
|
8836
|
+
ultra: {
|
|
8837
|
+
entitlements: [
|
|
8838
|
+
"core.cli.basic",
|
|
8839
|
+
"ai.chat.priority",
|
|
8840
|
+
"ai.code.advanced",
|
|
8841
|
+
"orchestrator.code",
|
|
8842
|
+
"orchestrator.media",
|
|
8843
|
+
"monitoring.analytics",
|
|
8844
|
+
"api.webhooks",
|
|
8845
|
+
"support.priority"
|
|
8846
|
+
],
|
|
8847
|
+
quota: {
|
|
8848
|
+
requests: 1e4,
|
|
8849
|
+
tokens: 5e6,
|
|
8850
|
+
code: 5e3,
|
|
8851
|
+
attachment: 1e3
|
|
8852
|
+
}
|
|
8853
|
+
}
|
|
8854
|
+
};
|
|
8855
|
+
var PLAN_ALIAS_MAP = {
|
|
8856
|
+
starterannual: "starter",
|
|
8857
|
+
"starter-annual": "starter",
|
|
8858
|
+
"starter_yearly": "starter",
|
|
8859
|
+
"starter-yearly": "starter",
|
|
8860
|
+
starteryearly: "starter",
|
|
8861
|
+
proannual: "pro",
|
|
8862
|
+
"pro-annual": "pro",
|
|
8863
|
+
"pro_yearly": "pro",
|
|
8864
|
+
"pro-yearly": "pro",
|
|
8865
|
+
proyearly: "pro",
|
|
8866
|
+
ultraannual: "ultra",
|
|
8867
|
+
"ultra-annual": "ultra",
|
|
8868
|
+
"ultra_yearly": "ultra",
|
|
8869
|
+
"ultra-yearly": "ultra",
|
|
8870
|
+
ultrayearly: "ultra"
|
|
8871
|
+
};
|
|
8872
|
+
function sanitizeKey(value) {
|
|
8873
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
8874
|
+
}
|
|
8875
|
+
function toIso(timestamp) {
|
|
8876
|
+
if (!timestamp) {
|
|
8877
|
+
return null;
|
|
8878
|
+
}
|
|
8879
|
+
return new Date(timestamp * 1e3).toISOString();
|
|
8880
|
+
}
|
|
8881
|
+
function formatPeriodId(date) {
|
|
8882
|
+
const instance = typeof date === "number" ? new Date(date) : date;
|
|
8883
|
+
const year = instance.getUTCFullYear();
|
|
8884
|
+
const month = String(instance.getUTCMonth() + 1).padStart(2, "0");
|
|
8885
|
+
return `${year}${month}`;
|
|
8886
|
+
}
|
|
8887
|
+
function extractId(value) {
|
|
8888
|
+
if (!value) {
|
|
8889
|
+
return void 0;
|
|
8890
|
+
}
|
|
8891
|
+
if (typeof value === "string") {
|
|
8892
|
+
return value;
|
|
8893
|
+
}
|
|
8894
|
+
return value.id ?? void 0;
|
|
8895
|
+
}
|
|
8896
|
+
function extractMetadataPlan(metadata) {
|
|
8897
|
+
if (!metadata) {
|
|
8898
|
+
return void 0;
|
|
8899
|
+
}
|
|
8900
|
+
const keys = ["planId", "plan_id", "plan", "plan-id", "plan_name"];
|
|
8901
|
+
for (const key of keys) {
|
|
8902
|
+
const value = metadata[key];
|
|
8903
|
+
if (value && value.trim()) {
|
|
8904
|
+
return value.trim();
|
|
8905
|
+
}
|
|
8906
|
+
}
|
|
8907
|
+
return void 0;
|
|
8908
|
+
}
|
|
8909
|
+
function extractUidFromMetadata(metadata) {
|
|
8910
|
+
if (!metadata) {
|
|
8911
|
+
return void 0;
|
|
8912
|
+
}
|
|
8913
|
+
const keys = ["uid", "userId", "user_id", "customer_uid"];
|
|
8914
|
+
for (const key of keys) {
|
|
8915
|
+
const value = metadata[key];
|
|
8916
|
+
if (value && value.trim()) {
|
|
8917
|
+
return value.trim();
|
|
8918
|
+
}
|
|
8919
|
+
}
|
|
8920
|
+
return void 0;
|
|
8921
|
+
}
|
|
8922
|
+
var StripeWebhookService = class {
|
|
8923
|
+
secret;
|
|
8924
|
+
toleranceSeconds;
|
|
8925
|
+
store;
|
|
8926
|
+
pricePlanMap = /* @__PURE__ */ new Map();
|
|
8927
|
+
productPlanMap = /* @__PURE__ */ new Map();
|
|
8928
|
+
constructor(options) {
|
|
8929
|
+
if (!options.secret) {
|
|
8930
|
+
throw new Error("Stripe webhook secret must be provided");
|
|
8931
|
+
}
|
|
8932
|
+
this.secret = options.secret;
|
|
8933
|
+
this.toleranceSeconds = options.toleranceSeconds ?? 300;
|
|
8934
|
+
this.store = options.store ?? new subscription_store_default();
|
|
8935
|
+
this.registerEnvPlanMappings();
|
|
8936
|
+
if (options.priceIdMap) {
|
|
8937
|
+
for (const [id, plan] of Object.entries(options.priceIdMap)) {
|
|
8938
|
+
this.pricePlanMap.set(sanitizeKey(id), plan);
|
|
8939
|
+
}
|
|
8940
|
+
}
|
|
8941
|
+
if (options.productIdMap) {
|
|
8942
|
+
for (const [id, plan] of Object.entries(options.productIdMap)) {
|
|
8943
|
+
this.productPlanMap.set(sanitizeKey(id), plan);
|
|
8944
|
+
}
|
|
8945
|
+
}
|
|
8946
|
+
}
|
|
8947
|
+
verifySignature(rawBody, signatureHeader) {
|
|
8948
|
+
if (!signatureHeader) {
|
|
8949
|
+
throw new Error("Stripe signature header is missing");
|
|
8950
|
+
}
|
|
8951
|
+
const header = Array.isArray(signatureHeader) ? signatureHeader.join(",") : signatureHeader;
|
|
8952
|
+
const { timestamp, signatures } = this.parseSignatureHeader(header);
|
|
8953
|
+
if (!timestamp || signatures.length === 0) {
|
|
8954
|
+
throw new Error("Stripe signature header is invalid");
|
|
8955
|
+
}
|
|
8956
|
+
const timestampSeconds = Number(timestamp);
|
|
8957
|
+
if (!Number.isFinite(timestampSeconds)) {
|
|
8958
|
+
throw new Error("Stripe signature timestamp is invalid");
|
|
8959
|
+
}
|
|
8960
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
8961
|
+
if (Math.abs(nowSeconds - timestampSeconds) > this.toleranceSeconds) {
|
|
8962
|
+
throw new Error("Stripe signature timestamp outside of tolerance window");
|
|
8963
|
+
}
|
|
8964
|
+
const signedPayload = `${timestamp}.${rawBody.toString("utf8")}`;
|
|
8965
|
+
const expected = crypto.createHmac("sha256", this.secret).update(signedPayload).digest("hex");
|
|
8966
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
8967
|
+
const isValid = signatures.some((signature) => {
|
|
8968
|
+
const signatureBuffer = Buffer.from(signature, "hex");
|
|
8969
|
+
if (signatureBuffer.length !== expectedBuffer.length) {
|
|
8970
|
+
return false;
|
|
8971
|
+
}
|
|
8972
|
+
return crypto.timingSafeEqual(signatureBuffer, expectedBuffer);
|
|
8973
|
+
});
|
|
8974
|
+
if (!isValid) {
|
|
8975
|
+
throw new Error("Stripe signature verification failed");
|
|
8976
|
+
}
|
|
8977
|
+
}
|
|
8978
|
+
parseEvent(rawBody) {
|
|
8979
|
+
try {
|
|
8980
|
+
const parsed = JSON.parse(rawBody.toString("utf8"));
|
|
8981
|
+
if (!parsed || typeof parsed !== "object" || !parsed.id || !parsed.type) {
|
|
8982
|
+
throw new Error("Invalid event payload");
|
|
8983
|
+
}
|
|
8984
|
+
return parsed;
|
|
8985
|
+
} catch (error) {
|
|
8986
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
8987
|
+
throw new Error(`Failed to parse Stripe event payload: ${message}`);
|
|
8988
|
+
}
|
|
8989
|
+
}
|
|
8990
|
+
async processEvent(event) {
|
|
8991
|
+
const receipt = await this.store.markEventReceived({
|
|
8992
|
+
id: event.id,
|
|
8993
|
+
type: event.type,
|
|
8994
|
+
created: event.created
|
|
8995
|
+
});
|
|
8996
|
+
if (receipt === "duplicate") {
|
|
8997
|
+
return;
|
|
8998
|
+
}
|
|
8999
|
+
try {
|
|
9000
|
+
switch (event.type) {
|
|
9001
|
+
case "checkout.session.completed":
|
|
9002
|
+
await this.handleCheckoutSession(event);
|
|
9003
|
+
break;
|
|
9004
|
+
case "customer.subscription.created":
|
|
9005
|
+
await this.handleSubscriptionCreated(event);
|
|
9006
|
+
break;
|
|
9007
|
+
case "customer.subscription.updated":
|
|
9008
|
+
await this.handleSubscriptionUpdated(event);
|
|
9009
|
+
break;
|
|
9010
|
+
case "customer.subscription.deleted":
|
|
9011
|
+
await this.handleSubscriptionDeleted(event);
|
|
9012
|
+
break;
|
|
9013
|
+
case "invoice.payment_succeeded":
|
|
9014
|
+
await this.handleInvoicePayment(event, true);
|
|
9015
|
+
break;
|
|
9016
|
+
case "invoice.payment_failed":
|
|
9017
|
+
await this.handleInvoicePayment(event, false);
|
|
9018
|
+
break;
|
|
9019
|
+
default:
|
|
9020
|
+
console.info(`Unhandled Stripe event type: ${event.type}`);
|
|
9021
|
+
}
|
|
9022
|
+
await this.store.markEventProcessed(event.id);
|
|
9023
|
+
} catch (error) {
|
|
9024
|
+
await this.store.markEventFailed(event.id, error);
|
|
9025
|
+
throw error;
|
|
9026
|
+
}
|
|
9027
|
+
}
|
|
9028
|
+
parseSignatureHeader(signatureHeader) {
|
|
9029
|
+
const parts = signatureHeader.split(",");
|
|
9030
|
+
let timestamp;
|
|
9031
|
+
const signatures = [];
|
|
9032
|
+
for (const part of parts) {
|
|
9033
|
+
const [key, value] = part.trim().split("=");
|
|
9034
|
+
if (key === "t") {
|
|
9035
|
+
timestamp = value;
|
|
9036
|
+
} else if (key.startsWith("v")) {
|
|
9037
|
+
signatures.push(value);
|
|
9038
|
+
}
|
|
9039
|
+
}
|
|
9040
|
+
return { timestamp, signatures };
|
|
9041
|
+
}
|
|
9042
|
+
registerEnvPlanMappings() {
|
|
9043
|
+
const mapping = [
|
|
9044
|
+
[process.env.STRIPE_PRICE_STARTER, "starter"],
|
|
9045
|
+
[process.env.STRIPE_PRICE_STARTER_ANNUAL, "starter"],
|
|
9046
|
+
[process.env.STRIPE_PRICE_PRO, "pro"],
|
|
9047
|
+
[process.env.STRIPE_PRICE_PRO_ANNUAL, "pro"],
|
|
9048
|
+
[process.env.STRIPE_PRICE_ULTRA, "ultra"],
|
|
9049
|
+
[process.env.STRIPE_PRICE_ULTRA_ANNUAL, "ultra"]
|
|
9050
|
+
];
|
|
9051
|
+
mapping.forEach(([id, plan]) => {
|
|
9052
|
+
if (id && id.trim()) {
|
|
9053
|
+
this.pricePlanMap.set(sanitizeKey(id), plan);
|
|
9054
|
+
}
|
|
9055
|
+
});
|
|
9056
|
+
const productMapping = [
|
|
9057
|
+
[process.env.STRIPE_PRODUCT_STARTER, "starter"],
|
|
9058
|
+
[process.env.STRIPE_PRODUCT_PRO, "pro"],
|
|
9059
|
+
[process.env.STRIPE_PRODUCT_ULTRA, "ultra"]
|
|
9060
|
+
];
|
|
9061
|
+
productMapping.forEach(([id, plan]) => {
|
|
9062
|
+
if (id && id.trim()) {
|
|
9063
|
+
this.productPlanMap.set(sanitizeKey(id), plan);
|
|
9064
|
+
}
|
|
9065
|
+
});
|
|
9066
|
+
}
|
|
9067
|
+
normalizePlanId(planId) {
|
|
9068
|
+
if (!planId) {
|
|
9069
|
+
return "free";
|
|
9070
|
+
}
|
|
9071
|
+
const key = sanitizeKey(planId);
|
|
9072
|
+
if (PLAN_CONFIG[key]) {
|
|
9073
|
+
return key;
|
|
9074
|
+
}
|
|
9075
|
+
const alias = PLAN_ALIAS_MAP[key];
|
|
9076
|
+
if (alias) {
|
|
9077
|
+
return alias;
|
|
9078
|
+
}
|
|
9079
|
+
return "free";
|
|
9080
|
+
}
|
|
9081
|
+
resolvePlanFromSubscription(subscription) {
|
|
9082
|
+
const metadataPlan = extractMetadataPlan(subscription.metadata);
|
|
9083
|
+
if (metadataPlan) {
|
|
9084
|
+
return this.normalizePlanId(metadataPlan);
|
|
9085
|
+
}
|
|
9086
|
+
const items = subscription.items?.data ?? [];
|
|
9087
|
+
for (const item of items) {
|
|
9088
|
+
const itemMetadataPlan = extractMetadataPlan(item.metadata);
|
|
9089
|
+
if (itemMetadataPlan) {
|
|
9090
|
+
return this.normalizePlanId(itemMetadataPlan);
|
|
9091
|
+
}
|
|
9092
|
+
const priceMetadataPlan = extractMetadataPlan(item.price?.metadata);
|
|
9093
|
+
if (priceMetadataPlan) {
|
|
9094
|
+
return this.normalizePlanId(priceMetadataPlan);
|
|
9095
|
+
}
|
|
9096
|
+
const planMetadataPlan = extractMetadataPlan(item.plan?.metadata);
|
|
9097
|
+
if (planMetadataPlan) {
|
|
9098
|
+
return this.normalizePlanId(planMetadataPlan);
|
|
9099
|
+
}
|
|
9100
|
+
const priceId = item.price?.id;
|
|
9101
|
+
if (priceId) {
|
|
9102
|
+
const mapped = this.pricePlanMap.get(sanitizeKey(priceId));
|
|
9103
|
+
if (mapped) {
|
|
9104
|
+
return mapped;
|
|
9105
|
+
}
|
|
9106
|
+
}
|
|
9107
|
+
const productId = extractId(item.price?.product ?? item.plan?.product ?? null);
|
|
9108
|
+
if (productId) {
|
|
9109
|
+
const mapped = this.productPlanMap.get(sanitizeKey(productId));
|
|
9110
|
+
if (mapped) {
|
|
9111
|
+
return mapped;
|
|
9112
|
+
}
|
|
9113
|
+
}
|
|
9114
|
+
const nickname = item.price?.nickname;
|
|
9115
|
+
if (nickname) {
|
|
9116
|
+
const candidate = this.normalizePlanId(nickname);
|
|
9117
|
+
if (candidate !== "free") {
|
|
9118
|
+
return candidate;
|
|
9119
|
+
}
|
|
9120
|
+
}
|
|
9121
|
+
}
|
|
9122
|
+
return "free";
|
|
9123
|
+
}
|
|
9124
|
+
getPlanConfig(planId) {
|
|
9125
|
+
return PLAN_CONFIG[planId] ?? PLAN_CONFIG.free;
|
|
9126
|
+
}
|
|
9127
|
+
async applyPlanEntitlements(uid, planId, subscription, overridePeriod) {
|
|
9128
|
+
const config = this.getPlanConfig(planId);
|
|
9129
|
+
await this.store.setEntitlements(uid, planId, config.entitlements);
|
|
9130
|
+
const periodStart = overridePeriod?.start ?? subscription?.current_period_start;
|
|
9131
|
+
overridePeriod?.end ?? subscription?.current_period_end;
|
|
9132
|
+
const periodId = periodStart ? formatPeriodId(new Date(periodStart * 1e3)) : formatPeriodId(Date.now());
|
|
9133
|
+
await this.store.setUsage({
|
|
9134
|
+
uid,
|
|
9135
|
+
planId,
|
|
9136
|
+
periodId,
|
|
9137
|
+
limits: config.quota
|
|
9138
|
+
});
|
|
9139
|
+
}
|
|
9140
|
+
comparePlanPriority(a, b) {
|
|
9141
|
+
return PLAN_PRIORITY[a] - PLAN_PRIORITY[b];
|
|
9142
|
+
}
|
|
9143
|
+
async resolveUid(customerId, ...metadataSources) {
|
|
9144
|
+
for (const metadata of metadataSources) {
|
|
9145
|
+
const uidFromMetadata = extractUidFromMetadata(metadata);
|
|
9146
|
+
if (uidFromMetadata) {
|
|
9147
|
+
return uidFromMetadata;
|
|
9148
|
+
}
|
|
9149
|
+
}
|
|
9150
|
+
if (customerId) {
|
|
9151
|
+
const customer = await this.store.getCustomer(customerId);
|
|
9152
|
+
return customer?.uid;
|
|
9153
|
+
}
|
|
9154
|
+
return void 0;
|
|
9155
|
+
}
|
|
9156
|
+
async handleCheckoutSession(event) {
|
|
9157
|
+
const session = event.data.object;
|
|
9158
|
+
const customerId = extractId(session.customer ?? null);
|
|
9159
|
+
const uid = await this.resolveUid(customerId, session.metadata);
|
|
9160
|
+
if (!customerId) {
|
|
9161
|
+
console.warn("checkout.session.completed received without customer id", { sessionId: session.id });
|
|
9162
|
+
return;
|
|
9163
|
+
}
|
|
9164
|
+
const metadata = {};
|
|
9165
|
+
if (session.metadata) {
|
|
9166
|
+
for (const [key, value] of Object.entries(session.metadata)) {
|
|
9167
|
+
if (typeof value === "string") {
|
|
9168
|
+
metadata[key] = value;
|
|
9169
|
+
}
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9172
|
+
await this.store.upsertCustomer({
|
|
9173
|
+
id: customerId,
|
|
9174
|
+
uid,
|
|
9175
|
+
email: session.customer_email ?? void 0,
|
|
9176
|
+
metadata: {
|
|
9177
|
+
...metadata,
|
|
9178
|
+
lastCheckoutSessionId: session.id,
|
|
9179
|
+
lastCheckoutMode: session.mode ?? void 0
|
|
9180
|
+
}
|
|
9181
|
+
});
|
|
9182
|
+
}
|
|
9183
|
+
async handleSubscriptionCreated(event) {
|
|
9184
|
+
const subscription = event.data.object;
|
|
9185
|
+
const customerId = extractId(subscription.customer);
|
|
9186
|
+
if (!customerId) {
|
|
9187
|
+
throw new Error(`Subscription ${subscription.id} missing customer id`);
|
|
9188
|
+
}
|
|
9189
|
+
const planId = this.resolvePlanFromSubscription(subscription);
|
|
9190
|
+
const uid = await this.resolveUid(customerId, subscription.metadata);
|
|
9191
|
+
const customerRecord = {
|
|
9192
|
+
id: customerId,
|
|
9193
|
+
uid,
|
|
9194
|
+
email: typeof subscription.customer === "object" ? subscription.customer.email ?? void 0 : void 0,
|
|
9195
|
+
metadata: subscription.metadata && Object.keys(subscription.metadata).length > 0 ? { ...subscription.metadata } : void 0,
|
|
9196
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9197
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9198
|
+
};
|
|
9199
|
+
await this.store.upsertCustomer(customerRecord);
|
|
9200
|
+
await this.store.upsertSubscription({
|
|
9201
|
+
id: subscription.id,
|
|
9202
|
+
customerId,
|
|
9203
|
+
uid,
|
|
9204
|
+
status: subscription.status,
|
|
9205
|
+
planId,
|
|
9206
|
+
pendingPlanId: null,
|
|
9207
|
+
pendingPlanEffectiveAt: null,
|
|
9208
|
+
cancelAt: toIso(subscription.cancel_at),
|
|
9209
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end ?? false,
|
|
9210
|
+
canceledAt: toIso(subscription.canceled_at),
|
|
9211
|
+
currentPeriodStart: toIso(subscription.current_period_start) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9212
|
+
currentPeriodEnd: toIso(subscription.current_period_end) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9213
|
+
trialEnd: toIso(subscription.trial_end),
|
|
9214
|
+
metadata: subscription.metadata && Object.keys(subscription.metadata).length > 0 ? { ...subscription.metadata } : void 0
|
|
9215
|
+
});
|
|
9216
|
+
if (uid) {
|
|
9217
|
+
await this.applyPlanEntitlements(uid, planId, subscription);
|
|
9218
|
+
}
|
|
9219
|
+
}
|
|
9220
|
+
async handleSubscriptionUpdated(event) {
|
|
9221
|
+
const subscription = event.data.object;
|
|
9222
|
+
const customerId = extractId(subscription.customer);
|
|
9223
|
+
if (!customerId) {
|
|
9224
|
+
throw new Error(`Subscription ${subscription.id} missing customer id`);
|
|
9225
|
+
}
|
|
9226
|
+
const existing = await this.store.getSubscription(subscription.id);
|
|
9227
|
+
const resolvedPlan = this.resolvePlanFromSubscription(subscription);
|
|
9228
|
+
const uid = await this.resolveUid(customerId, subscription.metadata, existing?.metadata);
|
|
9229
|
+
const planChange = existing ? this.comparePlanPriority(resolvedPlan, existing.planId) : 0;
|
|
9230
|
+
let effectivePlan = resolvedPlan;
|
|
9231
|
+
let pendingPlan = existing?.pendingPlanId ?? null;
|
|
9232
|
+
let pendingEffectiveAt = existing?.pendingPlanEffectiveAt ?? null;
|
|
9233
|
+
if (existing) {
|
|
9234
|
+
if (planChange < 0) {
|
|
9235
|
+
effectivePlan = existing.planId;
|
|
9236
|
+
pendingPlan = resolvedPlan;
|
|
9237
|
+
pendingEffectiveAt = toIso(subscription.current_period_end) ?? toIso(subscription.cancel_at) ?? existing.currentPeriodEnd;
|
|
9238
|
+
} else {
|
|
9239
|
+
pendingPlan = null;
|
|
9240
|
+
pendingEffectiveAt = null;
|
|
9241
|
+
if (planChange > 0 && uid) {
|
|
9242
|
+
await this.applyPlanEntitlements(uid, resolvedPlan, subscription);
|
|
9243
|
+
}
|
|
9244
|
+
}
|
|
9245
|
+
} else if (uid) {
|
|
9246
|
+
await this.applyPlanEntitlements(uid, resolvedPlan, subscription);
|
|
9247
|
+
}
|
|
9248
|
+
await this.store.upsertSubscription({
|
|
9249
|
+
id: subscription.id,
|
|
9250
|
+
customerId,
|
|
9251
|
+
uid,
|
|
9252
|
+
status: subscription.status,
|
|
9253
|
+
planId: effectivePlan,
|
|
9254
|
+
pendingPlanId: pendingPlan,
|
|
9255
|
+
pendingPlanEffectiveAt: pendingEffectiveAt,
|
|
9256
|
+
cancelAt: toIso(subscription.cancel_at),
|
|
9257
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end ?? false,
|
|
9258
|
+
canceledAt: toIso(subscription.canceled_at),
|
|
9259
|
+
currentPeriodStart: toIso(subscription.current_period_start) ?? existing?.currentPeriodStart ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9260
|
+
currentPeriodEnd: toIso(subscription.current_period_end) ?? existing?.currentPeriodEnd ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9261
|
+
trialEnd: toIso(subscription.trial_end),
|
|
9262
|
+
metadata: subscription.metadata && Object.keys(subscription.metadata).length > 0 ? { ...subscription.metadata } : void 0
|
|
9263
|
+
});
|
|
9264
|
+
if (uid) {
|
|
9265
|
+
await this.store.upsertCustomer({
|
|
9266
|
+
id: customerId,
|
|
9267
|
+
uid,
|
|
9268
|
+
metadata: subscription.metadata && Object.keys(subscription.metadata).length > 0 ? { ...subscription.metadata } : void 0
|
|
9269
|
+
});
|
|
9270
|
+
}
|
|
9271
|
+
}
|
|
9272
|
+
async handleSubscriptionDeleted(event) {
|
|
9273
|
+
const subscription = event.data.object;
|
|
9274
|
+
const customerId = extractId(subscription.customer);
|
|
9275
|
+
const existing = await this.store.getSubscription(subscription.id);
|
|
9276
|
+
const uid = await this.resolveUid(customerId, subscription.metadata, existing?.metadata);
|
|
9277
|
+
await this.store.upsertSubscription({
|
|
9278
|
+
id: subscription.id,
|
|
9279
|
+
customerId: customerId ?? existing?.customerId ?? "unknown",
|
|
9280
|
+
uid: uid ?? existing?.uid,
|
|
9281
|
+
status: "canceled",
|
|
9282
|
+
planId: "free",
|
|
9283
|
+
pendingPlanId: null,
|
|
9284
|
+
pendingPlanEffectiveAt: null,
|
|
9285
|
+
cancelAt: toIso(subscription.cancel_at),
|
|
9286
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end ?? false,
|
|
9287
|
+
canceledAt: toIso(subscription.canceled_at) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9288
|
+
currentPeriodStart: toIso(subscription.current_period_start) ?? existing?.currentPeriodStart ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9289
|
+
currentPeriodEnd: toIso(subscription.current_period_end) ?? existing?.currentPeriodEnd ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9290
|
+
trialEnd: toIso(subscription.trial_end),
|
|
9291
|
+
metadata: subscription.metadata && Object.keys(subscription.metadata).length > 0 ? { ...subscription.metadata } : void 0
|
|
9292
|
+
});
|
|
9293
|
+
if (uid) {
|
|
9294
|
+
await this.applyPlanEntitlements(uid, "free", subscription);
|
|
9295
|
+
}
|
|
9296
|
+
}
|
|
9297
|
+
async handleInvoicePayment(event, succeeded) {
|
|
9298
|
+
const invoice = event.data.object;
|
|
9299
|
+
const subscriptionId = extractId(invoice.subscription ?? null);
|
|
9300
|
+
if (!subscriptionId) {
|
|
9301
|
+
console.warn("Invoice received without subscription id", { invoiceId: invoice.id });
|
|
9302
|
+
return;
|
|
9303
|
+
}
|
|
9304
|
+
const subscription = await this.store.getSubscription(subscriptionId);
|
|
9305
|
+
if (!subscription) {
|
|
9306
|
+
console.warn("Invoice received for unknown subscription", { invoiceId: invoice.id, subscriptionId });
|
|
9307
|
+
return;
|
|
9308
|
+
}
|
|
9309
|
+
const customerId = extractId(invoice.customer ?? subscription.customerId);
|
|
9310
|
+
await this.store.recordInvoice({
|
|
9311
|
+
id: invoice.id,
|
|
9312
|
+
subscriptionId,
|
|
9313
|
+
customerId,
|
|
9314
|
+
amountPaid: (invoice.amount_paid ?? 0) / 100,
|
|
9315
|
+
currency: invoice.currency ?? "usd",
|
|
9316
|
+
status: succeeded ? "succeeded" : "failed",
|
|
9317
|
+
paidAt: invoice.created ? new Date(invoice.created * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
9318
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9319
|
+
periodStart: toIso(invoice.lines?.data?.[0]?.period?.start ?? invoice.period_start ?? null),
|
|
9320
|
+
periodEnd: toIso(invoice.lines?.data?.[0]?.period?.end ?? invoice.period_end ?? null),
|
|
9321
|
+
hostedInvoiceUrl: invoice.hosted_invoice_url ?? void 0
|
|
9322
|
+
});
|
|
9323
|
+
if (!succeeded) {
|
|
9324
|
+
await this.store.upsertSubscription({
|
|
9325
|
+
id: subscription.id,
|
|
9326
|
+
customerId: subscription.customerId,
|
|
9327
|
+
uid: subscription.uid,
|
|
9328
|
+
status: subscription.status,
|
|
9329
|
+
planId: subscription.planId,
|
|
9330
|
+
pendingPlanId: subscription.pendingPlanId ?? null,
|
|
9331
|
+
pendingPlanEffectiveAt: subscription.pendingPlanEffectiveAt ?? null,
|
|
9332
|
+
cancelAt: subscription.cancelAt ?? null,
|
|
9333
|
+
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd ?? false,
|
|
9334
|
+
canceledAt: subscription.canceledAt ?? null,
|
|
9335
|
+
currentPeriodStart: subscription.currentPeriodStart,
|
|
9336
|
+
currentPeriodEnd: subscription.currentPeriodEnd,
|
|
9337
|
+
trialEnd: subscription.trialEnd ?? null,
|
|
9338
|
+
latestInvoiceId: invoice.id,
|
|
9339
|
+
latestInvoiceStatus: "failed",
|
|
9340
|
+
latestInvoiceAmount: (invoice.amount_paid ?? 0) / 100
|
|
9341
|
+
});
|
|
9342
|
+
return;
|
|
9343
|
+
}
|
|
9344
|
+
const fallbackPeriodStart = subscription.currentPeriodStart ? Date.parse(subscription.currentPeriodStart) / 1e3 : void 0;
|
|
9345
|
+
const fallbackPeriodEnd = subscription.currentPeriodEnd ? Date.parse(subscription.currentPeriodEnd) / 1e3 : void 0;
|
|
9346
|
+
const periodStart = invoice.lines?.data?.[0]?.period?.start ?? invoice.period_start ?? fallbackPeriodStart;
|
|
9347
|
+
const periodEnd = invoice.lines?.data?.[0]?.period?.end ?? invoice.period_end ?? fallbackPeriodEnd;
|
|
9348
|
+
let nextPlanId = subscription.planId;
|
|
9349
|
+
let pendingPlanId = subscription.pendingPlanId ?? null;
|
|
9350
|
+
let pendingEffectiveAt = subscription.pendingPlanEffectiveAt ?? null;
|
|
9351
|
+
if (subscription.pendingPlanId) {
|
|
9352
|
+
nextPlanId = subscription.pendingPlanId;
|
|
9353
|
+
pendingPlanId = null;
|
|
9354
|
+
pendingEffectiveAt = null;
|
|
9355
|
+
if (subscription.uid) {
|
|
9356
|
+
await this.applyPlanEntitlements(subscription.uid, nextPlanId, null, { start: periodStart, end: periodEnd });
|
|
9357
|
+
}
|
|
9358
|
+
} else if (subscription.uid) {
|
|
9359
|
+
await this.applyPlanEntitlements(subscription.uid, subscription.planId, null, { start: periodStart, end: periodEnd });
|
|
9360
|
+
}
|
|
9361
|
+
await this.store.upsertSubscription({
|
|
9362
|
+
id: subscription.id,
|
|
9363
|
+
customerId: subscription.customerId,
|
|
9364
|
+
uid: subscription.uid,
|
|
9365
|
+
status: subscription.status,
|
|
9366
|
+
planId: nextPlanId,
|
|
9367
|
+
pendingPlanId,
|
|
9368
|
+
pendingPlanEffectiveAt: pendingEffectiveAt,
|
|
9369
|
+
cancelAt: subscription.cancelAt ?? null,
|
|
9370
|
+
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd ?? false,
|
|
9371
|
+
canceledAt: subscription.canceledAt ?? null,
|
|
9372
|
+
currentPeriodStart: periodStart ? toIso(periodStart) ?? subscription.currentPeriodStart : subscription.currentPeriodStart,
|
|
9373
|
+
currentPeriodEnd: periodEnd ? toIso(periodEnd) ?? subscription.currentPeriodEnd : subscription.currentPeriodEnd,
|
|
9374
|
+
trialEnd: subscription.trialEnd ?? null,
|
|
9375
|
+
latestInvoiceId: invoice.id,
|
|
9376
|
+
latestInvoiceStatus: "succeeded",
|
|
9377
|
+
latestInvoiceAmount: (invoice.amount_paid ?? 0) / 100
|
|
9378
|
+
});
|
|
9379
|
+
}
|
|
9380
|
+
};
|
|
9381
|
+
var stripe_webhook_service_default = StripeWebhookService;
|
|
8583
9382
|
|
|
8584
9383
|
// src/server/express-server.ts
|
|
8585
9384
|
var jobIndex = /* @__PURE__ */ new Map();
|
|
8586
9385
|
var app = express__default.default();
|
|
8587
9386
|
var port = process.env.PORT || 8080;
|
|
9387
|
+
var subscriptionStore = new subscription_store_default();
|
|
9388
|
+
var stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
9389
|
+
var stripeWebhookService = null;
|
|
9390
|
+
if (stripeWebhookSecret) {
|
|
9391
|
+
try {
|
|
9392
|
+
stripeWebhookService = new stripe_webhook_service_default({
|
|
9393
|
+
secret: stripeWebhookSecret,
|
|
9394
|
+
store: subscriptionStore
|
|
9395
|
+
});
|
|
9396
|
+
} catch (error) {
|
|
9397
|
+
stripeWebhookService = null;
|
|
9398
|
+
console.error("Failed to initialize Stripe webhook service:", error);
|
|
9399
|
+
}
|
|
9400
|
+
} else {
|
|
9401
|
+
console.warn("STRIPE_WEBHOOK_SECRET is not configured; Stripe webhook endpoint will be disabled.");
|
|
9402
|
+
}
|
|
8588
9403
|
app.use(helmet__default.default());
|
|
8589
9404
|
app.use(cors__default.default());
|
|
8590
9405
|
app.use(compression__default.default({
|
|
@@ -8593,6 +9408,7 @@ app.use(compression__default.default({
|
|
|
8593
9408
|
return compression.filter(req, res);
|
|
8594
9409
|
}
|
|
8595
9410
|
}));
|
|
9411
|
+
app.use("/api/stripe/webhook", express__default.default.raw({ type: "application/json" }));
|
|
8596
9412
|
app.use(express__default.default.json({ limit: "10mb" }));
|
|
8597
9413
|
app.use(express__default.default.urlencoded({ extended: true }));
|
|
8598
9414
|
try {
|
|
@@ -8621,7 +9437,7 @@ app.get("/api/status", (req, res) => {
|
|
|
8621
9437
|
app.get("/", (req, res) => {
|
|
8622
9438
|
res.json({
|
|
8623
9439
|
name: "MARIA CODE API",
|
|
8624
|
-
version: "4.3.
|
|
9440
|
+
version: "4.3.34",
|
|
8625
9441
|
status: "running",
|
|
8626
9442
|
environment: process.env.NODE_ENV || "development",
|
|
8627
9443
|
endpoints: {
|
|
@@ -8636,6 +9452,30 @@ app.get("/", (req, res) => {
|
|
|
8636
9452
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8637
9453
|
});
|
|
8638
9454
|
});
|
|
9455
|
+
app.post("/api/stripe/webhook", async (req, res) => {
|
|
9456
|
+
if (!stripeWebhookService) {
|
|
9457
|
+
return res.status(503).json({ error: "Stripe webhook processing is not configured" });
|
|
9458
|
+
}
|
|
9459
|
+
const signature = req.headers["stripe-signature"];
|
|
9460
|
+
const rawBody = Buffer.isBuffer(req.body) ? req.body : Buffer.from(typeof req.body === "string" ? req.body : JSON.stringify(req.body ?? {}), "utf8");
|
|
9461
|
+
try {
|
|
9462
|
+
stripeWebhookService.verifySignature(rawBody, signature);
|
|
9463
|
+
} catch (error) {
|
|
9464
|
+
console.error("Stripe webhook signature verification failed:", error);
|
|
9465
|
+
return res.status(400).json({ error: "Invalid Stripe signature" });
|
|
9466
|
+
}
|
|
9467
|
+
let event;
|
|
9468
|
+
try {
|
|
9469
|
+
event = stripeWebhookService.parseEvent(rawBody);
|
|
9470
|
+
} catch (error) {
|
|
9471
|
+
console.error("Stripe webhook payload parsing failed:", error);
|
|
9472
|
+
return res.status(400).json({ error: "Invalid Stripe payload" });
|
|
9473
|
+
}
|
|
9474
|
+
stripeWebhookService.processEvent(event).catch((error) => {
|
|
9475
|
+
console.error("Stripe webhook processing error:", error);
|
|
9476
|
+
});
|
|
9477
|
+
return res.sendStatus(200);
|
|
9478
|
+
});
|
|
8639
9479
|
function classifyMediaError(err) {
|
|
8640
9480
|
const raw = err;
|
|
8641
9481
|
const msg = String(raw?.message || raw || "unknown error");
|
|
@@ -8650,7 +9490,7 @@ function classifyMediaError(err) {
|
|
|
8650
9490
|
return { status: 422, code: "policy_violation", message: "Request was blocked by provider policy", hint: "Modify the prompt to comply with safety policies" };
|
|
8651
9491
|
}
|
|
8652
9492
|
if (lower2.includes("no inline image returned") || lower2.includes("no video returned") || lower2.includes("refus")) {
|
|
8653
|
-
return { status: 422, code: "content_refused", message: "Model refused or returned no content", hint: "Try rephrasing the prompt
|
|
9493
|
+
return { status: 422, code: "content_refused", message: "Model refused or returned no content", hint: "Try rephrasing the prompt" };
|
|
8654
9494
|
}
|
|
8655
9495
|
if (lower2.includes("timeout")) {
|
|
8656
9496
|
return { status: 504, code: "timeout", message: "Generation timed out", hint: "Please retry later" };
|
|
@@ -8684,6 +9524,106 @@ async function decodeFirebaseToken(token) {
|
|
|
8684
9524
|
return null;
|
|
8685
9525
|
}
|
|
8686
9526
|
}
|
|
9527
|
+
function getCurrentPeriodId() {
|
|
9528
|
+
const now = /* @__PURE__ */ new Date();
|
|
9529
|
+
return `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
9530
|
+
}
|
|
9531
|
+
function nextMonthResetISO() {
|
|
9532
|
+
const d = /* @__PURE__ */ new Date();
|
|
9533
|
+
d.setUTCMonth(d.getUTCMonth() + 1, 1);
|
|
9534
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
9535
|
+
return d.toISOString();
|
|
9536
|
+
}
|
|
9537
|
+
var PLAN_LIMITS = {
|
|
9538
|
+
free: { req: 300, tokens: 5e4, code: 40, attachment: 5 },
|
|
9539
|
+
starter: { req: 1400, tokens: 1e6, code: 300, attachment: 50 },
|
|
9540
|
+
"starter-annual": { req: 1400, tokens: 1e6, code: 300, attachment: 50 },
|
|
9541
|
+
pro: { req: 5e3, tokens: 35e5, code: 1200, attachment: 200 },
|
|
9542
|
+
"pro-annual": { req: 5e3, tokens: 35e5, code: 1200, attachment: 200 },
|
|
9543
|
+
ultra: { req: 1e4, tokens: 5e6, code: 5e3, attachment: 500 },
|
|
9544
|
+
"ultra-annual": { req: 1e4, tokens: 5e6, code: 5e3, attachment: 500 },
|
|
9545
|
+
enterprise: { req: -1, tokens: -1, code: -1, attachment: -1 }
|
|
9546
|
+
};
|
|
9547
|
+
function planName(planId) {
|
|
9548
|
+
const names = {
|
|
9549
|
+
free: "Free",
|
|
9550
|
+
starter: "Starter",
|
|
9551
|
+
"starter-annual": "Starter (\u5E74\u984D)",
|
|
9552
|
+
pro: "Pro",
|
|
9553
|
+
"pro-annual": "Pro (\u5E74\u984D)",
|
|
9554
|
+
ultra: "Ultra",
|
|
9555
|
+
"ultra-annual": "Ultra (\u5E74\u984D)",
|
|
9556
|
+
enterprise: "Enterprise"
|
|
9557
|
+
};
|
|
9558
|
+
return names[planId] || "Free";
|
|
9559
|
+
}
|
|
9560
|
+
async function getUserPlanAndLimits(uid) {
|
|
9561
|
+
const db = await getFirestoreSafe();
|
|
9562
|
+
const fallback = { planId: "free", limits: PLAN_LIMITS.free };
|
|
9563
|
+
if (!db) return fallback;
|
|
9564
|
+
try {
|
|
9565
|
+
const snap = await db.collection("user_subscriptions").doc(uid).get();
|
|
9566
|
+
const pid = snap.exists && snap.data()?.planId ? String(snap.data().planId) : "free";
|
|
9567
|
+
return { planId: pid, limits: PLAN_LIMITS[pid] || PLAN_LIMITS.free };
|
|
9568
|
+
} catch {
|
|
9569
|
+
return fallback;
|
|
9570
|
+
}
|
|
9571
|
+
}
|
|
9572
|
+
async function ensureUsageDoc(uid) {
|
|
9573
|
+
const db = await getFirestoreSafe();
|
|
9574
|
+
if (!db) return { ref: null, data: null, planId: "free", limits: PLAN_LIMITS.free };
|
|
9575
|
+
const periodId = getCurrentPeriodId();
|
|
9576
|
+
const { planId: pid, limits } = await getUserPlanAndLimits(uid);
|
|
9577
|
+
const ref = db.collection("users").doc(uid).collection("usage").doc(periodId);
|
|
9578
|
+
const snap = await ref.get();
|
|
9579
|
+
if (!snap.exists) {
|
|
9580
|
+
const nowISO = (/* @__PURE__ */ new Date()).toISOString();
|
|
9581
|
+
const init = {
|
|
9582
|
+
periodId,
|
|
9583
|
+
createdAt: nowISO,
|
|
9584
|
+
updatedAt: nowISO,
|
|
9585
|
+
resetAt: nextMonthResetISO(),
|
|
9586
|
+
limits,
|
|
9587
|
+
remain: limits,
|
|
9588
|
+
used: { req: 0, tokens: 0, code: 0, attachment: 0 }
|
|
9589
|
+
};
|
|
9590
|
+
await ref.set(init, { merge: true });
|
|
9591
|
+
return { ref, data: init, planId: pid, limits };
|
|
9592
|
+
}
|
|
9593
|
+
return { ref, data: snap.data(), planId: pid, limits };
|
|
9594
|
+
}
|
|
9595
|
+
async function applyConsumption(uid, consumption, idemKey) {
|
|
9596
|
+
const db = await getFirestoreSafe();
|
|
9597
|
+
if (!db) return null;
|
|
9598
|
+
const { ref, data, planId: pid, limits } = await ensureUsageDoc(uid);
|
|
9599
|
+
const nowISO = (/* @__PURE__ */ new Date()).toISOString();
|
|
9600
|
+
if (idemKey) {
|
|
9601
|
+
const idemRef = ref.collection("consumptions").doc(idemKey);
|
|
9602
|
+
const idemSnap = await idemRef.get();
|
|
9603
|
+
if (!idemSnap.exists) await idemRef.set({ createdAt: nowISO, consumption });
|
|
9604
|
+
else return data;
|
|
9605
|
+
}
|
|
9606
|
+
const toNum = (v) => typeof v === "number" && isFinite(v) ? v : 0;
|
|
9607
|
+
const incReq = toNum(consumption.requests) + toNum(consumption.req);
|
|
9608
|
+
const incTokens = toNum(consumption.tokens);
|
|
9609
|
+
const incCode = toNum(consumption.code) + toNum(consumption.image) + toNum(consumption.video);
|
|
9610
|
+
const incAttachment = toNum(consumption.attachment);
|
|
9611
|
+
const used = data?.used || { req: 0, tokens: 0, code: 0, attachment: 0 };
|
|
9612
|
+
const newUsed = {
|
|
9613
|
+
req: Math.max(0, used.req + incReq),
|
|
9614
|
+
tokens: Math.max(0, used.tokens + incTokens),
|
|
9615
|
+
code: Math.max(0, used.code + incCode),
|
|
9616
|
+
attachment: Math.max(0, used.attachment + incAttachment)
|
|
9617
|
+
};
|
|
9618
|
+
const remain = {
|
|
9619
|
+
req: limits.req < 0 ? -1 : Math.max(0, limits.req - newUsed.req),
|
|
9620
|
+
tokens: limits.tokens < 0 ? -1 : Math.max(0, limits.tokens - newUsed.tokens),
|
|
9621
|
+
code: limits.code < 0 ? -1 : Math.max(0, limits.code - newUsed.code),
|
|
9622
|
+
attachment: limits.attachment < 0 ? -1 : Math.max(0, limits.attachment - newUsed.attachment)
|
|
9623
|
+
};
|
|
9624
|
+
await ref.set({ used: newUsed, remain, updatedAt: nowISO }, { merge: true });
|
|
9625
|
+
return { ...data || {}, used: newUsed, remain, limits, periodId: getCurrentPeriodId(), updatedAt: nowISO };
|
|
9626
|
+
}
|
|
8687
9627
|
app.get("/api/user/profile", async (req, res) => {
|
|
8688
9628
|
try {
|
|
8689
9629
|
const authHeader = req.headers.authorization || "";
|
|
@@ -8704,19 +9644,21 @@ app.get("/api/user/profile", async (req, res) => {
|
|
|
8704
9644
|
if (Array.isArray(ids["github.com"]) && ids["github.com"].length > 0) provider = "github";
|
|
8705
9645
|
else if (Array.isArray(ids["google.com"]) && ids["google.com"].length > 0) provider = "google";
|
|
8706
9646
|
}
|
|
8707
|
-
const
|
|
9647
|
+
const { planId: pid, limits } = await getUserPlanAndLimits(uid);
|
|
9648
|
+
const periodRef = await ensureUsageDoc(uid);
|
|
9649
|
+
const currentUsedReq = Number(periodRef?.data?.used?.req || 0);
|
|
8708
9650
|
const response2 = {
|
|
8709
9651
|
id: uid,
|
|
8710
9652
|
email,
|
|
8711
9653
|
name: displayName,
|
|
8712
9654
|
provider: provider || "unknown",
|
|
8713
|
-
plan,
|
|
9655
|
+
plan: String(pid).toUpperCase(),
|
|
8714
9656
|
usage: {
|
|
8715
|
-
requests:
|
|
8716
|
-
requestLimit:
|
|
8717
|
-
tokens: 0,
|
|
8718
|
-
tokenLimit:
|
|
8719
|
-
resetDate:
|
|
9657
|
+
requests: currentUsedReq,
|
|
9658
|
+
requestLimit: limits.req < 0 ? Number.MAX_SAFE_INTEGER : limits.req,
|
|
9659
|
+
tokens: Number(periodRef?.data?.used?.tokens || 0),
|
|
9660
|
+
tokenLimit: limits.tokens < 0 ? Number.MAX_SAFE_INTEGER : limits.tokens,
|
|
9661
|
+
resetDate: periodRef?.data?.resetAt || nextMonthResetISO()
|
|
8720
9662
|
},
|
|
8721
9663
|
models: ["gpt-5", "gemini-2.5-pro"]
|
|
8722
9664
|
};
|
|
@@ -8827,7 +9769,9 @@ app.post("/api/v1/image", rateLimitMiddleware, async (req, res) => {
|
|
|
8827
9769
|
const decoded = await decodeFirebaseToken(idToken).catch(() => null);
|
|
8828
9770
|
if (!decoded) return res.status(401).json({ error: "unauthorized", message: "Invalid login session", hint: "Re-login to continue" });
|
|
8829
9771
|
const uid = decoded?.uid || decoded?.sub || "current";
|
|
8830
|
-
const { prompt, model, size = "1024x1024", format = "png", count = 1, seed } = req.body || {};
|
|
9772
|
+
const { prompt, model, size = "1024x1024", format: formatRaw = "png", count = 1, seed } = req.body || {};
|
|
9773
|
+
const fmt0 = String(formatRaw || "png").toLowerCase();
|
|
9774
|
+
const format = fmt0 === "jpeg" ? "jpg" : ["png", "jpg", "webp"].includes(fmt0) ? fmt0 : "png";
|
|
8831
9775
|
if (!prompt) return res.status(400).json({ error: "bad_request", message: "prompt required" });
|
|
8832
9776
|
const m2 = /^(\d{2,4})x(\d{2,4})$/.exec(String(size));
|
|
8833
9777
|
if (!m2) return res.status(400).json({ error: "bad_request", message: "size must be WxH" });
|
|
@@ -8837,7 +9781,13 @@ app.post("/api/v1/image", rateLimitMiddleware, async (req, res) => {
|
|
|
8837
9781
|
const buffers = [];
|
|
8838
9782
|
for (let i2 = 0; i2 < Math.max(1, Math.min(8, Number(count || 1))); i2++) {
|
|
8839
9783
|
const r2 = await provider.generateImage({ prompt, width: w, height: h2, format, seed: (seed ?? 0) + i2 });
|
|
8840
|
-
|
|
9784
|
+
const processed = await (await Promise.resolve().then(() => (init_image_post(), image_post_exports))).processImageOptional(
|
|
9785
|
+
r2.bytes,
|
|
9786
|
+
String(format),
|
|
9787
|
+
false,
|
|
9788
|
+
{ width: w, height: h2 }
|
|
9789
|
+
);
|
|
9790
|
+
buffers.push(processed);
|
|
8841
9791
|
}
|
|
8842
9792
|
const promptHash = hashPrompt(prompt);
|
|
8843
9793
|
const manifest = {
|
|
@@ -8858,10 +9808,10 @@ app.post("/api/v1/image", rateLimitMiddleware, async (req, res) => {
|
|
|
8858
9808
|
uid
|
|
8859
9809
|
});
|
|
8860
9810
|
const idemKey = req.headers["idempotency-key"] || void 0;
|
|
8861
|
-
await
|
|
9811
|
+
await applyConsumption(uid, { requests: 1, image: Math.max(1, buffers.length) }, idemKey);
|
|
8862
9812
|
const filesInline = buffers.map((b, idx) => ({
|
|
8863
9813
|
file: saved.files[idx] || "",
|
|
8864
|
-
mime: `image/${format}`,
|
|
9814
|
+
mime: format === "jpg" ? "image/jpeg" : `image/${format}`,
|
|
8865
9815
|
bytesBase64: b.toString("base64")
|
|
8866
9816
|
}));
|
|
8867
9817
|
return res.json({ success: true, data: { url: saved.manifestPath, files: saved.files, filesInline, jobId: manifest.trace } });
|
|
@@ -9103,7 +10053,7 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
|
|
|
9103
10053
|
const idToken = auth.substring("Bearer ".length).trim();
|
|
9104
10054
|
const decoded = await decodeFirebaseToken(idToken).catch(() => null);
|
|
9105
10055
|
const uid = decoded?.uid || decoded?.sub || "current";
|
|
9106
|
-
await
|
|
10056
|
+
await applyConsumption(uid, { requests: 1, video: 1 }, idemKey);
|
|
9107
10057
|
jobIndex.set(String(manifest.trace), {
|
|
9108
10058
|
id: String(manifest.trace),
|
|
9109
10059
|
status: "completed",
|
|
@@ -9156,6 +10106,12 @@ app.get("/api/v1/jobs/:id", async (req, res) => {
|
|
|
9156
10106
|
});
|
|
9157
10107
|
app.post("/api/v1/code", rateLimitMiddleware, async (req, res) => {
|
|
9158
10108
|
try {
|
|
10109
|
+
const auth = req.headers.authorization;
|
|
10110
|
+
if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
|
|
10111
|
+
const idToken = auth.substring("Bearer ".length).trim();
|
|
10112
|
+
const decoded = await decodeFirebaseToken(idToken).catch(() => null);
|
|
10113
|
+
if (!decoded) return res.status(401).json({ error: "unauthorized" });
|
|
10114
|
+
const uid = decoded?.uid || decoded?.sub;
|
|
9159
10115
|
const { prompt, language = "typescript", model = "gpt-4" } = req.body;
|
|
9160
10116
|
if (!prompt) {
|
|
9161
10117
|
return res.status(400).json({
|
|
@@ -9163,6 +10119,8 @@ app.post("/api/v1/code", rateLimitMiddleware, async (req, res) => {
|
|
|
9163
10119
|
message: "Prompt is required"
|
|
9164
10120
|
});
|
|
9165
10121
|
}
|
|
10122
|
+
const idemKey = req.headers["idempotency-key"] || void 0;
|
|
10123
|
+
await applyConsumption(uid, { requests: 1, code: 1 }, idemKey);
|
|
9166
10124
|
return res.json({
|
|
9167
10125
|
success: true,
|
|
9168
10126
|
data: {
|
|
@@ -9186,6 +10144,12 @@ function example() {
|
|
|
9186
10144
|
});
|
|
9187
10145
|
app.post("/api/v1/chat", rateLimitMiddleware, async (req, res) => {
|
|
9188
10146
|
try {
|
|
10147
|
+
const auth = req.headers.authorization;
|
|
10148
|
+
if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
|
|
10149
|
+
const idToken = auth.substring("Bearer ".length).trim();
|
|
10150
|
+
const decoded = await decodeFirebaseToken(idToken).catch(() => null);
|
|
10151
|
+
if (!decoded) return res.status(401).json({ error: "unauthorized" });
|
|
10152
|
+
const uid = decoded?.uid || decoded?.sub;
|
|
9189
10153
|
const { message, model = "gpt-4" } = req.body;
|
|
9190
10154
|
if (!message) {
|
|
9191
10155
|
return res.status(400).json({
|
|
@@ -9193,6 +10157,8 @@ app.post("/api/v1/chat", rateLimitMiddleware, async (req, res) => {
|
|
|
9193
10157
|
message: "Message is required"
|
|
9194
10158
|
});
|
|
9195
10159
|
}
|
|
10160
|
+
const idemKey = req.headers["idempotency-key"] || void 0;
|
|
10161
|
+
await applyConsumption(uid, { requests: 1 }, idemKey);
|
|
9196
10162
|
return res.json({
|
|
9197
10163
|
success: true,
|
|
9198
10164
|
data: {
|
|
@@ -9215,13 +10181,18 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
|
|
|
9215
10181
|
if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
|
|
9216
10182
|
const { prompt, taskType } = req.body || {};
|
|
9217
10183
|
if (!prompt) return res.status(400).json({ error: "bad_request", message: "prompt required" });
|
|
10184
|
+
const idToken = auth.substring("Bearer ".length).trim();
|
|
10185
|
+
const decoded = await decodeFirebaseToken(idToken).catch(() => null);
|
|
10186
|
+
if (!decoded) return res.status(401).json({ error: "unauthorized" });
|
|
10187
|
+
const uid = decoded?.uid || decoded?.sub;
|
|
10188
|
+
const idemKey = req.headers["idempotency-key"] || void 0;
|
|
9218
10189
|
if (process.env.MARIA_TELEMETRY === "1") {
|
|
9219
10190
|
try {
|
|
9220
10191
|
console.log(JSON.stringify({ ev: "ai_proxy_request", taskType: taskType || "unknown", promptLen: String(prompt).length }));
|
|
9221
10192
|
} catch {
|
|
9222
10193
|
}
|
|
9223
10194
|
}
|
|
9224
|
-
const
|
|
10195
|
+
const sanitizeKey2 = (v) => {
|
|
9225
10196
|
if (!v) return void 0;
|
|
9226
10197
|
let k = String(v).trim();
|
|
9227
10198
|
if (!k) return void 0;
|
|
@@ -9246,7 +10217,7 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
|
|
|
9246
10217
|
return {};
|
|
9247
10218
|
}
|
|
9248
10219
|
})();
|
|
9249
|
-
const gemKey =
|
|
10220
|
+
const gemKey = sanitizeKey2(keys?.googleApiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY);
|
|
9250
10221
|
if (gemKey) {
|
|
9251
10222
|
try {
|
|
9252
10223
|
const { GoogleGenerativeAI: GoogleGenerativeAI2 } = await import('@google/generative-ai');
|
|
@@ -9261,6 +10232,12 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
|
|
|
9261
10232
|
} catch {
|
|
9262
10233
|
}
|
|
9263
10234
|
}
|
|
10235
|
+
const consumption2 = { requests: 1 };
|
|
10236
|
+
if (taskType === "code" || taskType === "evaluation") consumption2.code = 1;
|
|
10237
|
+
try {
|
|
10238
|
+
await applyConsumption(uid, consumption2, idemKey);
|
|
10239
|
+
} catch {
|
|
10240
|
+
}
|
|
9264
10241
|
return res.json({ data: { content: content2, routedModel: { vendor: "google", family: "gemini", name: modelName, reason: taskType || "code" } } });
|
|
9265
10242
|
} catch (e2) {
|
|
9266
10243
|
console.warn("[AI Proxy] Gemini path failed, falling back to OpenAI:", e2?.message || e2);
|
|
@@ -9272,7 +10249,7 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
|
|
|
9272
10249
|
}
|
|
9273
10250
|
}
|
|
9274
10251
|
}
|
|
9275
|
-
const openaiKey =
|
|
10252
|
+
const openaiKey = sanitizeKey2(keys?.openaiApiKey || process.env.OPENAI_API_KEY);
|
|
9276
10253
|
if (!openaiKey) {
|
|
9277
10254
|
if (process.env.MARIA_TELEMETRY === "1") {
|
|
9278
10255
|
try {
|
|
@@ -9286,6 +10263,7 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
|
|
|
9286
10263
|
const client = new OpenAI2({ apiKey: openaiKey });
|
|
9287
10264
|
let model = process.env.MARIA_CODE_MODEL || "gpt-5-mini";
|
|
9288
10265
|
let content = "";
|
|
10266
|
+
let totalTokens = 0;
|
|
9289
10267
|
try {
|
|
9290
10268
|
const r2 = await client.responses.create({
|
|
9291
10269
|
model,
|
|
@@ -9295,6 +10273,12 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
|
|
|
9295
10273
|
]
|
|
9296
10274
|
});
|
|
9297
10275
|
content = r2?.output_text || r2?.content?.[0]?.text || "";
|
|
10276
|
+
const u = r2?.usage;
|
|
10277
|
+
if (u) {
|
|
10278
|
+
const inTok = Number(u.input_tokens || u.inputTokens || 0);
|
|
10279
|
+
const outTok = Number(u.output_tokens || u.outputTokens || 0);
|
|
10280
|
+
totalTokens = Number(u.total_tokens || u.totalTokens || inTok + outTok || 0);
|
|
10281
|
+
}
|
|
9298
10282
|
} catch (_e) {
|
|
9299
10283
|
model = "gpt-4o-mini";
|
|
9300
10284
|
const r2 = await client.chat.completions.create({
|
|
@@ -9305,6 +10289,8 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
|
|
|
9305
10289
|
]
|
|
9306
10290
|
});
|
|
9307
10291
|
content = r2.choices?.[0]?.message?.content || "";
|
|
10292
|
+
const u2 = r2?.usage;
|
|
10293
|
+
if (u2) totalTokens = Number(u2.total_tokens || 0);
|
|
9308
10294
|
}
|
|
9309
10295
|
if (process.env.MARIA_TELEMETRY === "1") {
|
|
9310
10296
|
try {
|
|
@@ -9312,6 +10298,13 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
|
|
|
9312
10298
|
} catch {
|
|
9313
10299
|
}
|
|
9314
10300
|
}
|
|
10301
|
+
const consumption = { requests: 1 };
|
|
10302
|
+
if (totalTokens > 0) consumption.tokens = totalTokens;
|
|
10303
|
+
if (taskType === "code" || taskType === "evaluation") consumption.code = 1;
|
|
10304
|
+
try {
|
|
10305
|
+
await applyConsumption(uid, consumption, idemKey);
|
|
10306
|
+
} catch {
|
|
10307
|
+
}
|
|
9315
10308
|
return res.json({ data: { content, routedModel: { vendor: "openai", family: "gpt", name: model, reason: taskType || "code" } } });
|
|
9316
10309
|
} catch (error) {
|
|
9317
10310
|
console.error("[AI Proxy] Error:", error);
|
|
@@ -9339,89 +10332,35 @@ async function getFirestoreSafe() {
|
|
|
9339
10332
|
return null;
|
|
9340
10333
|
}
|
|
9341
10334
|
}
|
|
9342
|
-
function calcNextReset() {
|
|
9343
|
-
const d = /* @__PURE__ */ new Date();
|
|
9344
|
-
d.setUTCMonth(d.getUTCMonth() + 1, 1);
|
|
9345
|
-
d.setUTCHours(0, 0, 0, 0);
|
|
9346
|
-
return d.toISOString();
|
|
9347
|
-
}
|
|
9348
|
-
async function recordConsumption(uid, consumption, idempotencyKey) {
|
|
9349
|
-
const db = await getFirestoreSafe();
|
|
9350
|
-
if (!db) return;
|
|
9351
|
-
const docPath = `projects/default/usage/${uid}`;
|
|
9352
|
-
const usageRef = db.doc(docPath);
|
|
9353
|
-
const nowISO = (/* @__PURE__ */ new Date()).toISOString();
|
|
9354
|
-
if (idempotencyKey) {
|
|
9355
|
-
const idemRef = db.doc(`projects/default/usage/${uid}/consumptions/${idempotencyKey}`);
|
|
9356
|
-
const idemSnap = await idemRef.get();
|
|
9357
|
-
if (idemSnap.exists) return;
|
|
9358
|
-
await idemRef.set({ createdAt: nowISO, consumption });
|
|
9359
|
-
}
|
|
9360
|
-
const snap = await usageRef.get();
|
|
9361
|
-
let data = snap.exists ? snap.data() : null;
|
|
9362
|
-
if (!data) {
|
|
9363
|
-
data = {
|
|
9364
|
-
plan: { name: "FREE", limits: { requests: free_plan_default.buckets.req } },
|
|
9365
|
-
monthly: {
|
|
9366
|
-
requests: { used: 0, limit: free_plan_default.buckets.req },
|
|
9367
|
-
image: { used: 0, limit: free_plan_default.buckets.image },
|
|
9368
|
-
video: { used: 0, limit: free_plan_default.buckets.video },
|
|
9369
|
-
code: { used: 0, limit: free_plan_default.buckets.code },
|
|
9370
|
-
resetAt: calcNextReset()
|
|
9371
|
-
},
|
|
9372
|
-
updatedAt: nowISO
|
|
9373
|
-
};
|
|
9374
|
-
}
|
|
9375
|
-
const m2 = data.monthly || {};
|
|
9376
|
-
if (consumption?.requests) m2.requests.used = Math.max(0, (m2.requests.used || 0) + Number(consumption.requests));
|
|
9377
|
-
if (consumption?.image) m2.image.used = Math.max(0, (m2.image.used || 0) + Number(consumption.image));
|
|
9378
|
-
if (consumption?.video) m2.video.used = Math.max(0, (m2.video.used || 0) + Number(consumption.video));
|
|
9379
|
-
if (consumption?.code) m2.code.used = Math.max(0, (m2.code.used || 0) + Number(consumption.code));
|
|
9380
|
-
data.monthly = m2;
|
|
9381
|
-
data.updatedAt = nowISO;
|
|
9382
|
-
await usageRef.set(data, { merge: true });
|
|
9383
|
-
}
|
|
9384
10335
|
app.get("/api/v1/usage", rateLimitMiddleware, async (req, res) => {
|
|
9385
10336
|
try {
|
|
9386
10337
|
const auth = req.headers.authorization;
|
|
9387
10338
|
if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
|
|
9388
|
-
const
|
|
9389
|
-
const
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
data
|
|
9412
|
-
}
|
|
9413
|
-
data = {
|
|
9414
|
-
plan: { name: "FREE", limits: { requests: free_plan_default.buckets.req } },
|
|
9415
|
-
monthly: {
|
|
9416
|
-
requests: { used: 0, limit: free_plan_default.buckets.req },
|
|
9417
|
-
image: { used: 0, limit: free_plan_default.buckets.image },
|
|
9418
|
-
video: { used: 0, limit: free_plan_default.buckets.video },
|
|
9419
|
-
code: { used: 0, limit: free_plan_default.buckets.code },
|
|
9420
|
-
resetAt: calcNextReset()
|
|
9421
|
-
}
|
|
9422
|
-
};
|
|
9423
|
-
}
|
|
9424
|
-
return res.json(data);
|
|
10339
|
+
const idToken = auth.substring("Bearer ".length).trim();
|
|
10340
|
+
const decoded = await decodeFirebaseToken(idToken).catch(() => null);
|
|
10341
|
+
if (!decoded) return res.status(401).json({ error: "unauthorized" });
|
|
10342
|
+
const uid = decoded?.uid || decoded?.sub;
|
|
10343
|
+
const { planId: pid, limits } = await getUserPlanAndLimits(uid);
|
|
10344
|
+
const { data } = await ensureUsageDoc(uid);
|
|
10345
|
+
const periodId = getCurrentPeriodId();
|
|
10346
|
+
const used = data?.used || { req: 0, tokens: 0, code: 0, attachment: 0 };
|
|
10347
|
+
const remain = data?.remain || limits;
|
|
10348
|
+
const percentage = {
|
|
10349
|
+
req: limits.req > 0 ? Math.round(used.req / limits.req * 100) : 0,
|
|
10350
|
+
tokens: limits.tokens > 0 ? Math.round(used.tokens / limits.tokens * 100) : 0,
|
|
10351
|
+
code: limits.code > 0 ? Math.round(used.code / limits.code * 100) : 0,
|
|
10352
|
+
attachment: limits.attachment > 0 ? Math.round(used.attachment / limits.attachment * 100) : 0
|
|
10353
|
+
};
|
|
10354
|
+
return res.json({
|
|
10355
|
+
periodId,
|
|
10356
|
+
planCode: pid,
|
|
10357
|
+
planName: planName(pid),
|
|
10358
|
+
used,
|
|
10359
|
+
remain,
|
|
10360
|
+
limits,
|
|
10361
|
+
percentage,
|
|
10362
|
+
resetAt: data?.resetAt || nextMonthResetISO()
|
|
10363
|
+
});
|
|
9425
10364
|
} catch (e2) {
|
|
9426
10365
|
console.error("[Usage] GET error", e2);
|
|
9427
10366
|
return res.status(500).json({ error: "internal_error" });
|
|
@@ -9433,18 +10372,23 @@ app.post("/api/v1/usage", rateLimitMiddleware, async (req, res) => {
|
|
|
9433
10372
|
if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
|
|
9434
10373
|
const { consumption } = req.body || {};
|
|
9435
10374
|
const idemKey = req.headers["idempotency-key"] || void 0;
|
|
9436
|
-
const
|
|
9437
|
-
const
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
10375
|
+
const idToken = auth.substring("Bearer ".length).trim();
|
|
10376
|
+
const decoded = await decodeFirebaseToken(idToken).catch(() => null);
|
|
10377
|
+
if (!decoded) return res.status(401).json({ error: "unauthorized" });
|
|
10378
|
+
const uid = decoded?.uid || decoded?.sub;
|
|
10379
|
+
await applyConsumption(uid, consumption || {}, idemKey);
|
|
10380
|
+
const { data } = await ensureUsageDoc(uid);
|
|
10381
|
+
const { planId: pid, limits } = await getUserPlanAndLimits(uid);
|
|
10382
|
+
const periodId = getCurrentPeriodId();
|
|
10383
|
+
const used = data?.used || { req: 0, tokens: 0, code: 0, attachment: 0 };
|
|
10384
|
+
const remain = data?.remain || limits;
|
|
10385
|
+
const percentage = {
|
|
10386
|
+
req: limits.req > 0 ? Math.round(used.req / limits.req * 100) : 0,
|
|
10387
|
+
tokens: limits.tokens > 0 ? Math.round(used.tokens / limits.tokens * 100) : 0,
|
|
10388
|
+
code: limits.code > 0 ? Math.round(used.code / limits.code * 100) : 0,
|
|
10389
|
+
attachment: limits.attachment > 0 ? Math.round(used.attachment / limits.attachment * 100) : 0
|
|
10390
|
+
};
|
|
10391
|
+
return res.json({ periodId, planCode: pid, planName: planName(pid), used, remain, limits, percentage, resetAt: data?.resetAt || nextMonthResetISO() });
|
|
9448
10392
|
} catch (e2) {
|
|
9449
10393
|
console.error("[Usage] POST error", e2);
|
|
9450
10394
|
return res.status(500).json({ error: "internal_error" });
|