vidspotai-shared 1.0.125 → 1.0.127

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.
@@ -15,7 +15,7 @@ export interface IUserModel {
15
15
  stripeCustomerId?: string;
16
16
  paymentSession?: {
17
17
  sessionId: string;
18
- paymentStatus: "processing" | "succeeded" | "failed";
18
+ paymentStatus: "processing" | "succeeded" | "failed" | "trial_blocked";
19
19
  lastUpdatedAt: Date;
20
20
  };
21
21
  metadata?: {
@@ -88,6 +88,12 @@ export interface IUserSubscription {
88
88
  trialUsed?: boolean;
89
89
  trialStart?: number | null;
90
90
  trialEnd?: number | null;
91
+ /**
92
+ * Set when a trial checkout was refused by the cross-account abuse guard
93
+ * (device / card / mailbox / network already used for another account's
94
+ * trial). The subscription is canceled and no trial credits are granted.
95
+ */
96
+ trialBlockedReason?: string;
91
97
  cancelAt?: number | null;
92
98
  /**
93
99
  * Raw Stripe subscription status (active | trialing | past_due | unpaid |
@@ -1 +1 @@
1
- {"version":3,"file":"user.model.d.ts","sourceRoot":"","sources":["../../src/models/user.model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EACL,SAAS,EACT,kBAAkB,EAElB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACxB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,YAAY,CAAC;IACtC,SAAS,EAAE,UAAU,CAAC;IACtB,SAAS,EAAE,UAAU,CAAC;IACtB,QAAQ,EAAE,YAAY,GAAG,UAAU,GAAG,WAAW,GAAG,MAAM,CAAC;IAC3D,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,YAAY,CAAC,EAAE,iBAAiB,CAAC;IACjC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE;QAEf,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAC;QACrD,aAAa,EAAE,IAAI,CAAC;KACrB,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,SAAS,CAAC,EAAE,UAAU,CAAC;QACvB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,uBAAuB,CAAC;IAClD;;;OAGG;IACH,wBAAwB,CAAC,EAAE,UAAU,CAAC;IACtC;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,UAAU,CAAC;IACnC;;;OAGG;IACH,UAAU,CAAC,EAAE;QACX,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;IACF;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,oBAAoB,CAAC;IAE7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAOrC"}
1
+ {"version":3,"file":"user.model.d.ts","sourceRoot":"","sources":["../../src/models/user.model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EACL,SAAS,EACT,kBAAkB,EAElB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACxB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,YAAY,CAAC;IACtC,SAAS,EAAE,UAAU,CAAC;IACtB,SAAS,EAAE,UAAU,CAAC;IACtB,QAAQ,EAAE,YAAY,GAAG,UAAU,GAAG,WAAW,GAAG,MAAM,CAAC;IAC3D,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,YAAY,CAAC,EAAE,iBAAiB,CAAC;IACjC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE;QAEf,SAAS,EAAE,MAAM,CAAC;QAGlB,aAAa,EAAE,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,eAAe,CAAC;QACvE,aAAa,EAAE,IAAI,CAAC;KACrB,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,SAAS,CAAC,EAAE,UAAU,CAAC;QACvB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,uBAAuB,CAAC;IAClD;;;OAGG;IACH,wBAAwB,CAAC,EAAE,UAAU,CAAC;IACtC;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,UAAU,CAAC;IACnC;;;OAGG;IACH,UAAU,CAAC,EAAE;QACX,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;IACF;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,oBAAoB,CAAC;IAE7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAOrC"}
@@ -1 +1 @@
1
- {"version":3,"file":"aiGenFactory.service.d.ts","sourceRoot":"","sources":["../../../src/services/aiGen/aiGenFactory.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EACL,iBAAiB,EAEjB,eAAe,EACf,aAAa,EACb,cAAc,EACd,qBAAqB,EAErB,qBAAqB,EAErB,eAAe,EACf,WAAW,EACZ,MAAM,wBAAwB,CAAC;AAgBhC,KAAK,uBAAuB,GAAG,UAAU,wBAAwB,CAAC;AASlE,eAAO,MAAM,gBAAgB,EAAE,OAAO,CACpC,MAAM,CAAC,eAAe,EAAE,uBAAuB,CAAC,CA+FjD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CACnC,iBAAiB,EACjB,uBAAuB,CAsDxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CACnC,eAAe,EACf,uBAAuB,CAMxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CACjC,aAAa,EACb,uBAAuB,CAGxB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,cAAc,EAAE,uBAAuB,CAsBzE,CAAC;AAEJ;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAC1C,qBAAqB,EACrB,uBAAuB,CAGxB,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAC1C,qBAAqB,EACrB,uBAAuB,CAGxB,CAAC;AAEF,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,qBAAqB,GAC9B,wBAAwB,CAM1B;AAED,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,qBAAqB,GAC9B,wBAAwB,CAM1B;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,WAAW,GACpB,wBAAwB,CAmB1B"}
1
+ {"version":3,"file":"aiGenFactory.service.d.ts","sourceRoot":"","sources":["../../../src/services/aiGen/aiGenFactory.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EACL,iBAAiB,EAEjB,eAAe,EACf,aAAa,EACb,cAAc,EACd,qBAAqB,EAErB,qBAAqB,EAErB,eAAe,EACf,WAAW,EACZ,MAAM,wBAAwB,CAAC;AAgBhC,KAAK,uBAAuB,GAAG,UAAU,wBAAwB,CAAC;AASlE,eAAO,MAAM,gBAAgB,EAAE,OAAO,CACpC,MAAM,CAAC,eAAe,EAAE,uBAAuB,CAAC,CAiGjD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CACnC,iBAAiB,EACjB,uBAAuB,CAsDxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CACnC,eAAe,EACf,uBAAuB,CAMxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CACjC,aAAa,EACb,uBAAuB,CAGxB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,cAAc,EAAE,uBAAuB,CAsBzE,CAAC;AAEJ;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAC1C,qBAAqB,EACrB,uBAAuB,CAGxB,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAC1C,qBAAqB,EACrB,uBAAuB,CAGxB,CAAC;AAEF,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,qBAAqB,GAC9B,wBAAwB,CAM1B;AAED,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,qBAAqB,GAC9B,wBAAwB,CAM1B;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,WAAW,GACpB,wBAAwB,CAmB1B"}
@@ -82,6 +82,8 @@ exports.videoServicesMap = {
82
82
  "kling-motion-control": kling_service_1.KlingService,
83
83
  "kling-lip-sync": kling_service_1.KlingService,
84
84
  // --- MiniMax ---
85
+ "minimax-h3": minimax_service_1.MinimaxService,
86
+ "minimax-h3-max": minimax_service_1.MinimaxService,
85
87
  "minimax-hailuo-2.3-fast": minimax_service_1.MinimaxService,
86
88
  "minimax-hailuo-2.3": minimax_service_1.MinimaxService,
87
89
  "minimax-hailuo-02": minimax_service_1.MinimaxService,
@@ -1 +1 @@
1
- {"version":3,"file":"bytedance.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/bytedance/bytedance.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA6BlB;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQlF;AAsBD,qBAAa,gBAAiB,SAAQ,wBAAwB;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CACsD;IAQ9E,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,WAAW;IAIb,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAoP3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAqIjD;;;;;;;;;;;OAWG;IACG,aAAa,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAyIlF,+EAA+E;YACjE,mBAAmB;IAoCjC;;;OAGG;YACW,eAAe;IAgD7B,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAsFjD"}
1
+ {"version":3,"file":"bytedance.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/bytedance/bytedance.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA8BlB;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQlF;AAsBD,qBAAa,gBAAiB,SAAQ,wBAAwB;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CACsD;IAQ9E,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,WAAW;IAIb,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA+Q3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAkJjD;;;;;;;;;;;OAWG;IACG,aAAa,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAyIlF,+EAA+E;YACjE,mBAAmB;IAoCjC;;;OAGG;YACW,eAAe;IAgD7B,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAsFjD"}
@@ -16,6 +16,7 @@ const image_model_1 = require("../../../../models/image.model");
16
16
  const helpers_2 = require("../../../../utils/helpers");
17
17
  const errors_1 = require("../../../../utils/errors");
18
18
  const logger_1 = require("../../../../utils/logger");
19
+ const transientRetry_1 = require("../../transientRetry");
19
20
  const crypto_1 = __importDefault(require("crypto"));
20
21
  const SEEDREAM_URL = "https://ark.ap-southeast.bytepluses.com/api/v3/images/generations";
21
22
  /** Long-edge-equivalent of each `size` tier (tier² = target pixel count). */
@@ -197,13 +198,32 @@ class ByteDanceService extends baseAiGenProvider_service_1.BaseAiGenProviderServ
197
198
  .digest("hex")
198
199
  .slice(0, 16);
199
200
  }
200
- const resp = await fetch(this.baseUrl, {
201
- method: "POST",
202
- headers: {
203
- "Content-Type": "application/json",
204
- Authorization: `Bearer ${process.env.BYTEDANCE_API_KEY}`,
205
- },
206
- body: JSON.stringify(body),
201
+ // ModelArk intermittently answers task creation with a 500
202
+ // InternalServiceError (prod 2026-10-06, several in an hour) that
203
+ // succeeds on resubmit. Retry 5xx only; 4xx stays permanent and 429 is
204
+ // left to the worker's rate limiter.
205
+ const resp = await (0, transientRetry_1.withTransientRetry)("bytedance:generateVideo", async () => {
206
+ const r = await fetch(this.baseUrl, {
207
+ method: "POST",
208
+ headers: {
209
+ "Content-Type": "application/json",
210
+ Authorization: `Bearer ${process.env.BYTEDANCE_API_KEY}`,
211
+ },
212
+ body: JSON.stringify(body),
213
+ });
214
+ if (r.status >= 500) {
215
+ throw new transientRetry_1.TransientHttpError(r.status, `ByteDance generateVideo failed (${r.status}): ${await r.text()}`);
216
+ }
217
+ return r;
218
+ }, {
219
+ baseBackoffMs: 2000,
220
+ onRetry: ({ attempt, maxAttempts, backoffMs, err }) => logger_1.logger.warn("Seedance: transient create error, retrying", {
221
+ modelKey: params.modelKey,
222
+ attempt,
223
+ maxAttempts,
224
+ backoffMs,
225
+ error: err.message.slice(0, 300),
226
+ }),
207
227
  });
208
228
  if (!resp.ok) {
209
229
  const errText = await resp.text();
@@ -310,6 +330,17 @@ class ByteDanceService extends baseAiGenProvider_service_1.BaseAiGenProviderServ
310
330
  errorMessage: JSON.stringify(data.error),
311
331
  };
312
332
  }
333
+ // ModelArk copies the input image to its own storage and transcodes it
334
+ // (`x-tos-process=image/format,jpg`); an image it can't decode fails
335
+ // asynchronously as InvalidParameter "Error while downloading: …ark-…
336
+ // status code: 400" (prod 2026-10-06, a user-uploaded PNG). That's the
337
+ // input file, not our system — tell the user to re-export it.
338
+ if (errCode === "InvalidParameter" && /error while downloading/i.test(errMessage)) {
339
+ return {
340
+ status: types_1.EVideoSceneStatus.FAILED,
341
+ errorMessage: "user_input_error: This model couldn't process your input image. Please re-save it as a standard JPEG or PNG (or use a different image) and try again.",
342
+ };
343
+ }
313
344
  return {
314
345
  status: types_1.EVideoSceneStatus.FAILED,
315
346
  errorMessage: data.error
@@ -1 +1 @@
1
- {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAuIlB,qBAAa,aAAc,SAAQ,wBAAwB;IAKzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAQ/C;;;;;;;;;OASG;IACG,YAAY,CAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IAoChC;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAoCvB;;;;OAIG;YACW,kBAAkB;IAsC1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAiUjC;;;;;;;OAOG;YACW,iBAAiB;YAoGjB,eAAe;IA4EvB,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAyEjD;;;;OAIG;YACW,oBAAoB;IAkH5B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;YAiBnB,cAAc;IAyM5B;;;;;;OAMG;IACH;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAIjC,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,SAAa,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,iBAAiB,GAAG,MAAM;CAoCpJ"}
1
+ {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAuIlB,qBAAa,aAAc,SAAQ,wBAAwB;IAKzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAQ/C;;;;;;;;;OASG;IACG,YAAY,CAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IAoChC;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAoCvB;;;;OAIG;YACW,kBAAkB;IAsC1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAiUjC;;;;;;;OAOG;YACW,iBAAiB;YAoGjB,eAAe;IA4EvB,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAyEjD;;;;OAIG;YACW,oBAAoB;IAkH5B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;YAiBnB,cAAc;IAkP5B;;;;;;OAMG;IACH;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAIjC,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,SAAa,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,iBAAiB,GAAG,MAAM;CAoCpJ"}
@@ -836,6 +836,9 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
836
836
  // 200 with the images filtered out — so the reason is only available here.
837
837
  // Mirrors the Veo video path (raiMediaFilteredReasons, above).
838
838
  const blockReasons = [];
839
+ // Nano Banana declining with a text-only answer instead of an image —
840
+ // see the 2026-09-30 comment below where this is populated.
841
+ const textDeclines = [];
839
842
  if (isImagen) {
840
843
  const resp = await this.ai.models.generateImages({
841
844
  model: modelId,
@@ -929,6 +932,7 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
929
932
  })));
930
933
  for (const resp of responses) {
931
934
  const candidates = resp.candidates ?? [];
935
+ let gotImageThisResponse = false;
932
936
  for (const cand of candidates) {
933
937
  for (const part of cand.content?.parts ?? []) {
934
938
  const inline = part.inlineData;
@@ -937,6 +941,7 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
937
941
  bytes: Buffer.from(inline.data, "base64"),
938
942
  mime: inline.mimeType ?? "image/png",
939
943
  });
944
+ gotImageThisResponse = true;
940
945
  }
941
946
  }
942
947
  }
@@ -954,6 +959,28 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
954
959
  blockReasons.push(finish);
955
960
  }
956
961
  }
962
+ // 2026-09-30: gemini-3.1-flash-image (Nano Banana 2) refuses some
963
+ // requests with finishReason "STOP" and NO promptFeedback block — it
964
+ // just answers with a TEXT part instead of an image. Verified by
965
+ // replaying prod job 9NyH60DhQD5wZiS6UenP (an edit asking to undress
966
+ // a person in an uploaded photo): candidates[0] = one text part, in
967
+ // the user's language ("Saya tidak bisa mengubah gambar ... karena
968
+ // adanya pelanggaran kebijakan terkait konten seksual"),
969
+ // finishReason STOP, no promptFeedback. Neither block signal above
970
+ // fires, so this used to fall through to the generic "no image
971
+ // bytes" Error and page Slack on every refusal. Harvest the model's
972
+ // text as the reason. It is logged (warn) for triage but never shown
973
+ // to the user — it is model-language-specific free text.
974
+ if (!gotImageThisResponse) {
975
+ const declineText = candidates
976
+ .flatMap((cand) => cand.content?.parts ?? [])
977
+ .map((part) => part.text)
978
+ .filter((text) => Boolean(text && text.trim()))
979
+ .join(" ")
980
+ .trim();
981
+ if (declineText)
982
+ textDeclines.push(declineText);
983
+ }
957
984
  }
958
985
  }
959
986
  else {
@@ -974,8 +1001,20 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
974
1001
  });
975
1002
  throw new errors_1.UserFacingError("Your prompt was flagged by Google's safety filters. Please rephrase and try again.", errors_1.USER_FACING_ERROR_CODES.CONTENT_POLICY_VIOLATION);
976
1003
  }
977
- // No block reason at all — genuinely unexpected; keep it loud so a real
978
- // platform/SDK regression isn't muted by the branch above.
1004
+ if (textDeclines.length) {
1005
+ logger_1.logger.warn("Google image generation declined with text instead of an image", {
1006
+ modelKey: params.modelKey,
1007
+ modelId,
1008
+ declines: textDeclines,
1009
+ });
1010
+ // A text-only STOP from an image model is a refusal (policy), same
1011
+ // outcome as the safety-filter branch above, so same code: the
1012
+ // frontend already maps it to "flagged by the model's safety filters".
1013
+ throw new errors_1.UserFacingError("Your prompt was flagged by Google's safety filters. Please rephrase and try again.", errors_1.USER_FACING_ERROR_CODES.CONTENT_POLICY_VIOLATION);
1014
+ }
1015
+ // No block reason and no text at all — genuinely unexpected; keep it
1016
+ // loud so a real platform/SDK regression isn't muted by the branches
1017
+ // above.
979
1018
  throw new Error("Google image generation returned no image bytes");
980
1019
  }
981
1020
  const ts = Date.now();
@@ -1 +1 @@
1
- {"version":3,"file":"runway.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/runway/runway.service.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAWlB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;YAUlB,OAAO;IAgCf,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAyJ3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAuDjD,aAAa,CAAC,EACZ,QAAQ,EACR,QAAY,EACZ,SAAiB,EACjB,SAAa,EACb,WAAW,EACX,WAAW,EACX,UAAU,EACV,KAAK,EACL,aAAa,EACb,qBAAqB,EACrB,cAAkB,EAClB,cAAkB,GACnB,EAAE,iBAAiB,GAAG,MAAM;IAwD7B;;;;;;;;OAQG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;CA+ElC"}
1
+ {"version":3,"file":"runway.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/runway/runway.service.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAWlB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;YAUlB,OAAO;IA8Cf,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAyJ3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAuDjD,aAAa,CAAC,EACZ,QAAQ,EACR,QAAY,EACZ,SAAiB,EACjB,SAAa,EACb,WAAW,EACX,WAAW,EACX,UAAU,EACV,KAAK,EACL,aAAa,EACb,qBAAqB,EACrB,cAAkB,EAClB,cAAkB,GACnB,EAAE,iBAAiB,GAAG,MAAM;IAwD7B;;;;;;;;OAQG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;CA+ElC"}
@@ -41,6 +41,14 @@ class RunwayService extends baseAiGenProvider_service_1.BaseAiGenProviderService
41
41
  validateStatus: () => true, // manually handle status
42
42
  });
43
43
  if (response.status < 200 || response.status >= 300) {
44
+ // Runway validates promptText length server-side (1000 chars on its
45
+ // native models; prod 2026-10-06 runway-gen4.5). That's the user's
46
+ // prompt, not a system fault — surface it like every other provider.
47
+ const issues = response.status === 400 && Array.isArray(response.data?.issues) ? response.data.issues : [];
48
+ const tooLong = issues.find((i) => i.code === "too_big" && Array.isArray(i.path) && i.path[0] === "promptText");
49
+ if (tooLong) {
50
+ throw new errors_1.UserFacingError(`Your prompt is too long for this model${tooLong.maximum ? ` (max ${tooLong.maximum} characters)` : ""}. Please shorten it and try again.`, errors_1.USER_FACING_ERROR_CODES.PROMPT_TOO_LONG);
51
+ }
44
52
  throw new Error(`Runway API Error: ${response.status} ${response.statusText} - ${JSON.stringify(response.data)}`);
45
53
  }
46
54
  return response.data;
@@ -38,6 +38,7 @@ export * from "./socialAI";
38
38
  export * from "./socialInsights";
39
39
  export * from "./socialAccounts";
40
40
  export * from "./freePlan";
41
+ export * from "./trialGuard";
41
42
  export * from "./organizationCredit.service";
42
43
  export * from "./email";
43
44
  export * from "./assetPool";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,OAAO,CAAC;AACtB,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qCAAqC,CAAC;AACpD,cAAc,qBAAqB,CAAC;AACpC,cAAc,OAAO,CAAC;AACtB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,OAAO,CAAC;AACtB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,6CAA6C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,OAAO,CAAC;AACtB,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qCAAqC,CAAC;AACpD,cAAc,qBAAqB,CAAC;AACpC,cAAc,OAAO,CAAC;AACtB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,OAAO,CAAC;AACtB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,6CAA6C,CAAC"}
@@ -54,6 +54,7 @@ __exportStar(require("./socialAI"), exports);
54
54
  __exportStar(require("./socialInsights"), exports);
55
55
  __exportStar(require("./socialAccounts"), exports);
56
56
  __exportStar(require("./freePlan"), exports);
57
+ __exportStar(require("./trialGuard"), exports);
57
58
  __exportStar(require("./organizationCredit.service"), exports);
58
59
  __exportStar(require("./email"), exports);
59
60
  __exportStar(require("./assetPool"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"translation.service.d.ts","sourceRoot":"","sources":["../../../src/services/translation/translation.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAiDvD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAkB,GAAG,MAAM,EAAE,CA8BjF;AA4ID;;;;;;GAMG;AACH,iBAAe,SAAS,CACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,EACpB,EAAE,GAAE,cAAkC,EACtC,OAAO,GAAE,IAAI,GAAG,IAAW,GAC1B,OAAO,CAAC,MAAM,CAAC,CAiBjB;AAiGD;;;;;;;;;;;;;;GAcG;AACH,iBAAe,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8ChE;AAOD,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC;AAGF,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,kBAAY,CAAC;AAC/C,0DAA0D;AAC1D,eAAO,MAAM,UAAU,uBAAiB,CAAC"}
1
+ {"version":3,"file":"translation.service.d.ts","sourceRoot":"","sources":["../../../src/services/translation/translation.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAiDvD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAkB,GAAG,MAAM,EAAE,CA8BjF;AA4ID;;;;;;GAMG;AACH,iBAAe,SAAS,CACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,EACpB,EAAE,GAAE,cAAkC,EACtC,OAAO,GAAE,IAAI,GAAG,IAAW,GAC1B,OAAO,CAAC,MAAM,CAAC,CAiBjB;AA+ID;;;;;;;;;;;;;;GAcG;AACH,iBAAe,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8ChE;AAOD,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC;AAGF,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,kBAAY,CAAC;AAC/C,0DAA0D;AAC1D,eAAO,MAAM,UAAU,uBAAiB,CAAC"}
@@ -276,33 +276,72 @@ async function translate(text, from, to = types_1.ELANGUAGE_CODE.en, version = "
276
276
  });
277
277
  return translated.join("");
278
278
  }
279
- /** One chunk through primary → LLM fallback. */
279
+ // 2026-10-07: chunking fixed the long-prompt failures, but the two layers
280
+ // still ran strictly in sequence, so a primary that was merely SLOW (not down)
281
+ // burned up to 2 × primaryTimeoutFor() ≈ 17s before the LLM even started —
282
+ // prod paged "slow step >30s: getTranslationWrapper" at 30-40s for short `bg`
283
+ // / `ru` prompts. Hedge instead: if the primary hasn't answered within
284
+ // HEDGE_DELAY_MS (or fails sooner), start the LLM alongside it and take
285
+ // whichever succeeds first. Healthy primaries answer well inside the delay,
286
+ // so the LLM still only runs on the slow/failing tail.
287
+ const HEDGE_DELAY_MS = 3000;
288
+ /** Resolves with the first promise to fulfil; rejects with every error only
289
+ * once all of them have rejected (Promise.any without the ES2021 lib). */
290
+ function firstSuccess(promises) {
291
+ return new Promise((resolve, reject) => {
292
+ const errors = new Array(promises.length);
293
+ let pending = promises.length;
294
+ promises.forEach((p, i) => {
295
+ p.then(resolve, (err) => {
296
+ errors[i] = err;
297
+ if (--pending === 0)
298
+ reject(errors);
299
+ });
300
+ });
301
+ });
302
+ }
303
+ /** One chunk through primary, hedged with the LLM fallback. */
280
304
  async function translateChunk(text, from, to, version) {
281
305
  // ── Layer 1: primary Cloud Function ──────────────────────────────────────
282
306
  let primaryErr;
283
- for (let attempt = 1; attempt <= PRIMARY_MAX_ATTEMPTS; attempt++) {
284
- try {
285
- return await callPrimaryTranslate(text, from, to, version);
286
- }
287
- catch (err) {
288
- primaryErr = err;
289
- if (attempt < PRIMARY_MAX_ATTEMPTS) {
290
- await new Promise((r) => setTimeout(r, 200 * attempt));
307
+ const primary = (async () => {
308
+ for (let attempt = 1; attempt <= PRIMARY_MAX_ATTEMPTS; attempt++) {
309
+ try {
310
+ return await callPrimaryTranslate(text, from, to, version);
311
+ }
312
+ catch (err) {
313
+ primaryErr = err;
314
+ if (attempt < PRIMARY_MAX_ATTEMPTS) {
315
+ await new Promise((r) => setTimeout(r, 200 * attempt));
316
+ }
291
317
  }
292
318
  }
293
- }
319
+ throw primaryErr;
320
+ })();
294
321
  // ── Layer 2: LLM fallback (the common recovery path — WARN, not Slack) ────
295
- logger_1.logger.warn("translation: primary failed, falling back to LLM", {
296
- sourceLang: from,
297
- targetLang: to,
298
- primaryAttempts: PRIMARY_MAX_ATTEMPTS,
299
- fallbackModel: FALLBACK_MODEL,
300
- err: errMsg(primaryErr),
322
+ // Starts on primary failure or after HEDGE_DELAY_MS, whichever comes first.
323
+ let startFallback;
324
+ const fallbackGate = new Promise((r) => (startFallback = r));
325
+ const hedgeTimer = setTimeout(() => startFallback(), HEDGE_DELAY_MS);
326
+ primary.catch(() => startFallback());
327
+ const fallback = fallbackGate.then(() => {
328
+ logger_1.logger.warn(primaryErr
329
+ ? "translation: primary failed, falling back to LLM"
330
+ : "translation: primary slow, hedging with LLM", {
331
+ sourceLang: from,
332
+ targetLang: to,
333
+ primaryAttempts: PRIMARY_MAX_ATTEMPTS,
334
+ hedgeDelayMs: HEDGE_DELAY_MS,
335
+ fallbackModel: FALLBACK_MODEL,
336
+ err: primaryErr ? errMsg(primaryErr) : undefined,
337
+ });
338
+ return callLlmTranslate(text, from, to);
301
339
  });
302
340
  try {
303
- return await callLlmTranslate(text, from, to);
341
+ return await firstSuccess([primary, fallback]);
304
342
  }
305
- catch (fallbackErr) {
343
+ catch (errors) {
344
+ const [, fallbackErr] = errors;
306
345
  // Both layers down — this is the only case worth a Slack alert.
307
346
  logger_1.logger.error("translation failed on BOTH primary and LLM fallback", {
308
347
  sourceLang: from,
@@ -314,6 +353,9 @@ async function translateChunk(text, from, to, version) {
314
353
  e.cause = fallbackErr;
315
354
  throw e;
316
355
  }
356
+ finally {
357
+ clearTimeout(hedgeTimer);
358
+ }
317
359
  }
318
360
  /** One un-retried LLM detect attempt. */
319
361
  async function callLlmDetectOnce(text) {
@@ -0,0 +1,2 @@
1
+ export * from "./trialSignals";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/trialGuard/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./trialSignals"), exports);
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Pure (no I/O) half of the free-trial abuse guard: turns a request + user into
3
+ * a set of identity signals, and decides eligibility from previously recorded
4
+ * trial claims. The Firestore half lives in the functions app
5
+ * (services/trialGuard/trialGuard.service.ts).
6
+ *
7
+ * Why several signals: each one alone is easy to dodge or too blunt.
8
+ * - deviceId random id the browser keeps in localStorage + a cookie. Exact,
9
+ * never collides between two people, but cleared by incognito /
10
+ * clearing site data.
11
+ * - fp + ip browser fingerprint (FingerprintJS OSS). Survives incognito,
12
+ * but identical phones of the same model produce the same value,
13
+ * so on its own it would block strangers. Paired with the network
14
+ * it means "same browser on the same network".
15
+ * - card Stripe card fingerprint — identical for the same physical card
16
+ * across Stripe customers. Strongest signal; only known after
17
+ * Checkout, so it's enforced in the webhook.
18
+ * - email normalized mailbox (gmail dots / +tags folded) so a.b+1@gmail
19
+ * and ab@gmail are one person.
20
+ * - ip soft cap: at most N trials per network per window. Kept loose
21
+ * because mobile carriers put many real users behind one IP.
22
+ * - disposable throwaway / lab inbox domains never get a trial.
23
+ */
24
+ export type TrialClaimKind = "device" | "fpip" | "card" | "email" | "ip";
25
+ export interface TrialSignals {
26
+ deviceId?: string;
27
+ fp?: string;
28
+ ip?: string;
29
+ email?: string;
30
+ cardFingerprint?: string;
31
+ }
32
+ export interface TrialClaimEvent {
33
+ userId: string;
34
+ at: number;
35
+ }
36
+ /** A recorded claim doc in `trialClaims/{kind}_{hash}`. */
37
+ export interface TrialClaimDoc {
38
+ kind: TrialClaimKind;
39
+ userIds: string[];
40
+ events: TrialClaimEvent[];
41
+ }
42
+ export type TrialDenyReason = "disposable_email" | "device_reused" | "browser_network_reused" | "card_reused" | "email_reused" | "ip_limit";
43
+ export type TrialDecision = {
44
+ eligible: true;
45
+ } | {
46
+ eligible: false;
47
+ reason: TrialDenyReason;
48
+ };
49
+ export declare const TRIAL_IP_WINDOW_MS: number;
50
+ export declare const TRIAL_MAX_PER_IP_DEFAULT = 2;
51
+ /** Most recent claim events kept per doc — bounds doc size for busy IPs. */
52
+ export declare const TRIAL_CLAIM_EVENTS_CAP = 20;
53
+ export declare function normalizeEmail(raw: string): string | undefined;
54
+ export declare function isThrowawayEmail(email: string | undefined): boolean;
55
+ /**
56
+ * Network key for an IP. IPv6 is cut to its /64: one subscriber line / phone
57
+ * usually gets a whole /64 and rotates the low bits, so the full address
58
+ * would make every request look like a new network.
59
+ */
60
+ export declare function networkKey(ip: string | undefined): string | undefined;
61
+ /**
62
+ * Client IP on Cloud Run / Cloud Functions v2. Google's front end APPENDS the
63
+ * real client address to X-Forwarded-For, so anything to its left is
64
+ * client-supplied and spoofable — take the rightmost entry, not [0].
65
+ */
66
+ export declare function clientIpFromHeaders(forwardedFor: string | string[] | undefined, socketAddress: string | undefined): string | undefined;
67
+ export declare function sanitizeDeviceId(v: unknown): string | undefined;
68
+ export declare function sanitizeFingerprint(v: unknown): string | undefined;
69
+ export interface ClaimKey {
70
+ kind: TrialClaimKind;
71
+ docId: string;
72
+ }
73
+ export declare function claimKeys(signals: TrialSignals): ClaimKey[];
74
+ /**
75
+ * Decide whether `userId` may start a trial given the existing claim docs for
76
+ * its signals (`existing` is keyed by docId; missing = never claimed). A claim
77
+ * held only by this same user never blocks, so webhook retries stay idempotent.
78
+ */
79
+ export declare function decideTrial(params: {
80
+ userId: string;
81
+ signals: TrialSignals;
82
+ existing: Map<string, TrialClaimDoc | undefined>;
83
+ now: number;
84
+ maxPerIp?: number;
85
+ }): TrialDecision;
86
+ /** The claim doc after `userId` claims it (idempotent for repeat claims). */
87
+ export declare function withClaim(doc: TrialClaimDoc | undefined, kind: TrialClaimKind, userId: string, now: number): TrialClaimDoc;
88
+ //# sourceMappingURL=trialSignals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trialSignals.d.ts","sourceRoot":"","sources":["../../../src/services/trialGuard/trialSignals.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAEzE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,2DAA2D;AAC3D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,MAAM,eAAe,GACvB,kBAAkB,GAClB,eAAe,GACf,wBAAwB,GACxB,aAAa,GACb,cAAc,GACd,UAAU,CAAC;AAEf,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,GAClB;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAAC;AAEjD,eAAO,MAAM,kBAAkB,QAA2B,CAAC;AAC3D,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,KAAK,CAAC;AASzC,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAY9D;AAUD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAOnE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAYrE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAC3C,aAAa,EAAE,MAAM,GAAG,SAAS,GAChC,MAAM,GAAG,SAAS,CAIpB;AAKD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE/D;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAElE;AAOD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,QAAQ,EAAE,CAY3D;AASD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC,CAAC;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,aAAa,CAyBhB;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CACvB,GAAG,EAAE,aAAa,GAAG,SAAS,EAC9B,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GACV,aAAa,CASf"}
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TRIAL_CLAIM_EVENTS_CAP = exports.TRIAL_MAX_PER_IP_DEFAULT = exports.TRIAL_IP_WINDOW_MS = void 0;
4
+ exports.normalizeEmail = normalizeEmail;
5
+ exports.isThrowawayEmail = isThrowawayEmail;
6
+ exports.networkKey = networkKey;
7
+ exports.clientIpFromHeaders = clientIpFromHeaders;
8
+ exports.sanitizeDeviceId = sanitizeDeviceId;
9
+ exports.sanitizeFingerprint = sanitizeFingerprint;
10
+ exports.claimKeys = claimKeys;
11
+ exports.decideTrial = decideTrial;
12
+ exports.withClaim = withClaim;
13
+ const crypto_1 = require("crypto");
14
+ const disposable_email_domains_js_1 = require("disposable-email-domains-js");
15
+ exports.TRIAL_IP_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
16
+ exports.TRIAL_MAX_PER_IP_DEFAULT = 2;
17
+ /** Most recent claim events kept per doc — bounds doc size for busy IPs. */
18
+ exports.TRIAL_CLAIM_EVENTS_CAP = 20;
19
+ // Inbox domains that are throwaway in practice but missing from the public
20
+ // disposable list. qwiklabs.net = Google Cloud Skills Boost lab accounts,
21
+ // auto-generated per lab session (seen abusing trials 2026-10-05).
22
+ const EXTRA_BLOCKED_DOMAINS = ["qwiklabs.net"];
23
+ const GMAIL_DOMAINS = new Set(["gmail.com", "googlemail.com"]);
24
+ function normalizeEmail(raw) {
25
+ const email = raw.trim().toLowerCase();
26
+ const at = email.lastIndexOf("@");
27
+ if (at <= 0 || at === email.length - 1)
28
+ return undefined;
29
+ let local = email.slice(0, at);
30
+ let domain = email.slice(at + 1);
31
+ local = local.split("+")[0] ?? "";
32
+ if (GMAIL_DOMAINS.has(domain)) {
33
+ local = local.replace(/\./g, "");
34
+ domain = "gmail.com";
35
+ }
36
+ return local ? `${local}@${domain}` : undefined;
37
+ }
38
+ function blockedDomains() {
39
+ const fromEnv = (process.env.TRIAL_BLOCKED_EMAIL_DOMAINS ?? "")
40
+ .split(",")
41
+ .map((d) => d.trim().toLowerCase())
42
+ .filter(Boolean);
43
+ return [...EXTRA_BLOCKED_DOMAINS, ...fromEnv];
44
+ }
45
+ function isThrowawayEmail(email) {
46
+ if (!email)
47
+ return false;
48
+ const domain = email.trim().toLowerCase().split("@").pop() ?? "";
49
+ if (!domain)
50
+ return false;
51
+ if ((0, disposable_email_domains_js_1.isDisposableEmailDomain)(domain))
52
+ return true;
53
+ // Match the domain itself or any subdomain of a blocked domain.
54
+ return blockedDomains().some((d) => domain === d || domain.endsWith(`.${d}`));
55
+ }
56
+ /**
57
+ * Network key for an IP. IPv6 is cut to its /64: one subscriber line / phone
58
+ * usually gets a whole /64 and rotates the low bits, so the full address
59
+ * would make every request look like a new network.
60
+ */
61
+ function networkKey(ip) {
62
+ if (!ip)
63
+ return undefined;
64
+ let addr = ip.trim();
65
+ if (addr.startsWith("::ffff:"))
66
+ addr = addr.slice(7); // IPv4-mapped
67
+ if (!addr)
68
+ return undefined;
69
+ if (!addr.includes(":"))
70
+ return addr;
71
+ const [head, tail = ""] = addr.split("::");
72
+ const headParts = head ? head.split(":") : [];
73
+ const tailParts = tail ? tail.split(":") : [];
74
+ const fill = Array(Math.max(0, 8 - headParts.length - tailParts.length)).fill("0");
75
+ const groups = addr.includes("::") ? [...headParts, ...fill, ...tailParts] : headParts;
76
+ return `${groups.slice(0, 4).map((g) => g.replace(/^0+(?=.)/, "")).join(":")}::/64`;
77
+ }
78
+ /**
79
+ * Client IP on Cloud Run / Cloud Functions v2. Google's front end APPENDS the
80
+ * real client address to X-Forwarded-For, so anything to its left is
81
+ * client-supplied and spoofable — take the rightmost entry, not [0].
82
+ */
83
+ function clientIpFromHeaders(forwardedFor, socketAddress) {
84
+ const raw = Array.isArray(forwardedFor) ? forwardedFor.join(",") : forwardedFor;
85
+ const parts = (raw ?? "").split(",").map((p) => p.trim()).filter(Boolean);
86
+ return parts[parts.length - 1] || socketAddress || undefined;
87
+ }
88
+ const DEVICE_ID_RE = /^[A-Za-z0-9-]{16,64}$/;
89
+ const FP_RE = /^[A-Za-z0-9]{16,64}$/;
90
+ function sanitizeDeviceId(v) {
91
+ return typeof v === "string" && DEVICE_ID_RE.test(v) ? v : undefined;
92
+ }
93
+ function sanitizeFingerprint(v) {
94
+ return typeof v === "string" && FP_RE.test(v) ? v.toLowerCase() : undefined;
95
+ }
96
+ /** Raw values are hashed before storage — the claims collection never holds an IP or email. */
97
+ function hash(value) {
98
+ return (0, crypto_1.createHash)("sha256").update(value).digest("hex").slice(0, 40);
99
+ }
100
+ function claimKeys(signals) {
101
+ const keys = [];
102
+ const add = (kind, value) => {
103
+ if (value)
104
+ keys.push({ kind, docId: `${kind}_${hash(`${kind}:${value}`)}` });
105
+ };
106
+ const net = networkKey(signals.ip);
107
+ add("device", signals.deviceId);
108
+ add("fpip", signals.fp && net ? `${signals.fp}|${net}` : undefined);
109
+ add("card", signals.cardFingerprint);
110
+ add("email", signals.email ? normalizeEmail(signals.email) : undefined);
111
+ add("ip", net);
112
+ return keys;
113
+ }
114
+ const EXACT_REASON = {
115
+ device: "device_reused",
116
+ fpip: "browser_network_reused",
117
+ card: "card_reused",
118
+ email: "email_reused",
119
+ };
120
+ /**
121
+ * Decide whether `userId` may start a trial given the existing claim docs for
122
+ * its signals (`existing` is keyed by docId; missing = never claimed). A claim
123
+ * held only by this same user never blocks, so webhook retries stay idempotent.
124
+ */
125
+ function decideTrial(params) {
126
+ const { userId, signals, existing, now } = params;
127
+ const maxPerIp = params.maxPerIp ?? exports.TRIAL_MAX_PER_IP_DEFAULT;
128
+ if (isThrowawayEmail(signals.email)) {
129
+ return { eligible: false, reason: "disposable_email" };
130
+ }
131
+ for (const key of claimKeys(signals)) {
132
+ const doc = existing.get(key.docId);
133
+ if (!doc)
134
+ continue;
135
+ if (key.kind === "ip") {
136
+ const others = new Set(doc.events
137
+ .filter((e) => e.userId !== userId && now - e.at < exports.TRIAL_IP_WINDOW_MS)
138
+ .map((e) => e.userId));
139
+ if (others.size >= maxPerIp)
140
+ return { eligible: false, reason: "ip_limit" };
141
+ continue;
142
+ }
143
+ if (doc.userIds.some((id) => id !== userId)) {
144
+ return { eligible: false, reason: EXACT_REASON[key.kind] };
145
+ }
146
+ }
147
+ return { eligible: true };
148
+ }
149
+ /** The claim doc after `userId` claims it (idempotent for repeat claims). */
150
+ function withClaim(doc, kind, userId, now) {
151
+ const userIds = doc?.userIds ?? [];
152
+ const events = doc?.events ?? [];
153
+ if (userIds.includes(userId))
154
+ return { kind, userIds, events };
155
+ return {
156
+ kind,
157
+ userIds: [...userIds, userId],
158
+ events: [...events, { userId, at: now }].slice(-exports.TRIAL_CLAIM_EVENTS_CAP),
159
+ };
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidspotai-shared",
3
- "version": "1.0.125",
3
+ "version": "1.0.127",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "exports": {
@@ -23,6 +23,7 @@
23
23
  "audio-decode": "^3.10.1",
24
24
  "axios": "^1.12.2",
25
25
  "bullmq": "^5.61.0",
26
+ "disposable-email-domains-js": "^1.26.0",
26
27
  "essentia.js": "^0.1.3",
27
28
  "firebase-admin": "^13.5.0",
28
29
  "fs-extra": "^11.3.2",