vidspotai-shared 1.0.125 → 1.0.126
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/providers/google/google.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/google/google.service.js +41 -2
- 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/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":"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();
|
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);
|
|
@@ -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.126",
|
|
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",
|