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.
@@ -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":"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();
@@ -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);
@@ -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.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",