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.
- package/lib/models/user.model.d.ts +7 -1
- package/lib/models/user.model.d.ts.map +1 -1
- package/lib/services/aiGen/aiGenFactory.service.d.ts.map +1 -1
- package/lib/services/aiGen/aiGenFactory.service.js +2 -0
- package/lib/services/aiGen/providers/bytedance/bytedance.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/bytedance/bytedance.service.js +38 -7
- package/lib/services/aiGen/providers/google/google.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/google/google.service.js +41 -2
- package/lib/services/aiGen/providers/runway/runway.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/runway/runway.service.js +8 -0
- package/lib/services/index.d.ts +1 -0
- package/lib/services/index.d.ts.map +1 -1
- package/lib/services/index.js +1 -0
- package/lib/services/translation/translation.service.d.ts.map +1 -1
- package/lib/services/translation/translation.service.js +60 -18
- package/lib/services/trialGuard/index.d.ts +2 -0
- package/lib/services/trialGuard/index.d.ts.map +1 -0
- package/lib/services/trialGuard/index.js +17 -0
- package/lib/services/trialGuard/trialSignals.d.ts +88 -0
- package/lib/services/trialGuard/trialSignals.d.ts.map +1 -0
- package/lib/services/trialGuard/trialSignals.js +160 -0
- package/package.json +2 -1
|
@@ -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;
|
|
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,
|
|
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;
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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;
|
|
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
|
-
|
|
978
|
-
|
|
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;
|
|
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;
|
package/lib/services/index.d.ts
CHANGED
|
@@ -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"}
|
package/lib/services/index.js
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
341
|
+
return await firstSuccess([primary, fallback]);
|
|
304
342
|
}
|
|
305
|
-
catch (
|
|
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 @@
|
|
|
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.
|
|
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",
|