@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.
@@ -5158,22 +5158,22 @@ var init_from = __esm({
5158
5158
  init_file();
5159
5159
  init_fetch_blob();
5160
5160
  ({ stat } = fs.promises);
5161
- blobFromSync = (path3, type) => fromBlob(fs.statSync(path3), path3, type);
5162
- blobFrom = (path3, type) => stat(path3).then((stat2) => fromBlob(stat2, path3, type));
5163
- fileFrom = (path3, type) => stat(path3).then((stat2) => fromFile(stat2, path3, type));
5164
- fileFromSync = (path3, type) => fromFile(fs.statSync(path3), path3, type);
5165
- fromBlob = (stat2, path3, type = "") => new fetch_blob_default([new BlobDataItem({
5166
- path: path3,
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, path3, type = "") => new file_default([new BlobDataItem({
5172
- path: path3,
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(path3), { type, lastModified: stat2.mtimeMs });
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(path3) {
6848
- if (path3.includes("/image")) return "/image";
6849
- if (path3.includes("/video")) return "/video";
6850
- if (path3.includes("/code")) return "/code";
6851
- if (path3.includes("/chat")) return "/chat";
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: `image/${req.format || "png"}`
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 : `image/${req.format}` };
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: `image/${req.format || "png"}` };
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.32",
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 or a different model" };
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 plan = "FREE";
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: 0,
8716
- requestLimit: 1e3,
8717
- tokens: 0,
8718
- tokenLimit: 5e5,
8719
- resetDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString()
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
- buffers.push(r2.bytes);
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 recordConsumption(uid, { requests: 1, image: Math.max(1, buffers.length) }, idemKey);
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 recordConsumption(uid, { requests: 1, video: 1 }, idemKey);
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 sanitizeKey = (v) => {
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 = sanitizeKey(keys?.googleApiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY);
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 = sanitizeKey(keys?.openaiApiKey || process.env.OPENAI_API_KEY);
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 db = await getFirestoreSafe();
9389
- const uid = "current";
9390
- const docPath = `projects/default/usage/${uid}`;
9391
- let data = null;
9392
- if (db) {
9393
- const snap = await db.doc(docPath).get();
9394
- data = snap.exists ? snap.data() : null;
9395
- if (!data) {
9396
- data = {
9397
- plan: { name: "FREE", limits: { requests: free_plan_default.buckets.req } },
9398
- monthly: {
9399
- requests: { used: 0, limit: free_plan_default.buckets.req },
9400
- image: { used: 0, limit: free_plan_default.buckets.image },
9401
- video: { used: 0, limit: free_plan_default.buckets.video },
9402
- code: { used: 0, limit: free_plan_default.buckets.code },
9403
- resetAt: calcNextReset()
9404
- },
9405
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9406
- };
9407
- await db.doc(docPath).set(data);
9408
- }
9409
- const historySnap = await db.collection(`projects/default/usage/${uid}/consumptions`).orderBy("createdAt", "desc").limit(20).get();
9410
- const history = historySnap.docs.map((d) => ({ id: d.id, ...d.data() }));
9411
- data.history = history;
9412
- } else {
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 db = await getFirestoreSafe();
9437
- const uid = "current";
9438
- const docPath = `projects/default/usage/${uid}`;
9439
- let data = null;
9440
- if (db) {
9441
- await recordConsumption(uid, consumption || {}, idemKey);
9442
- const snap = await db.doc(docPath).get();
9443
- data = snap.exists ? snap.data() : { ok: true };
9444
- } else {
9445
- data = { ok: true };
9446
- }
9447
- return res.json(data);
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" });