vidspotai-shared 1.0.124 → 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.
Files changed (153) hide show
  1. package/lib/globals/aiModels/enums.d.ts +38 -0
  2. package/lib/globals/aiModels/enums.d.ts.map +1 -1
  3. package/lib/globals/aiModels/enums.js +49 -1
  4. package/lib/globals/aiModels/index.d.ts.map +1 -1
  5. package/lib/globals/aiModels/index.js +2 -0
  6. package/lib/globals/aiModels/providers/alibaba.d.ts.map +1 -1
  7. package/lib/globals/aiModels/providers/alibaba.js +460 -53
  8. package/lib/globals/aiModels/providers/bytedance.d.ts.map +1 -1
  9. package/lib/globals/aiModels/providers/bytedance.js +221 -39
  10. package/lib/globals/aiModels/providers/google.d.ts.map +1 -1
  11. package/lib/globals/aiModels/providers/google.js +178 -32
  12. package/lib/globals/aiModels/providers/kling.d.ts.map +1 -1
  13. package/lib/globals/aiModels/providers/kling.js +86 -12
  14. package/lib/globals/aiModels/providers/minimax.d.ts.map +1 -1
  15. package/lib/globals/aiModels/providers/minimax.js +77 -0
  16. package/lib/globals/aiModels/providers/openai.d.ts.map +1 -1
  17. package/lib/globals/aiModels/providers/openai.js +12 -0
  18. package/lib/globals/aiModels/providers/pixverse.d.ts.map +1 -1
  19. package/lib/globals/aiModels/providers/pixverse.js +101 -28
  20. package/lib/globals/aiModels/providers/runway.d.ts.map +1 -1
  21. package/lib/globals/aiModels/providers/runway.js +450 -24
  22. package/lib/globals/aiModels/providers/xai.d.ts +4 -0
  23. package/lib/globals/aiModels/providers/xai.d.ts.map +1 -0
  24. package/lib/globals/aiModels/providers/xai.js +72 -0
  25. package/lib/globals/aiModels/tierHelpers.d.ts +10 -0
  26. package/lib/globals/aiModels/tierHelpers.d.ts.map +1 -1
  27. package/lib/globals/aiModels/tierHelpers.js +48 -7
  28. package/lib/globals/aiModels/types.d.ts +21 -0
  29. package/lib/globals/aiModels/types.d.ts.map +1 -1
  30. package/lib/models/image.model.d.ts +24 -0
  31. package/lib/models/image.model.d.ts.map +1 -1
  32. package/lib/models/image.model.js +12 -0
  33. package/lib/models/index.d.ts +1 -0
  34. package/lib/models/index.d.ts.map +1 -1
  35. package/lib/models/index.js +1 -0
  36. package/lib/models/klingElement.model.d.ts +52 -0
  37. package/lib/models/klingElement.model.d.ts.map +1 -0
  38. package/lib/models/klingElement.model.js +24 -0
  39. package/lib/models/user.model.d.ts +7 -1
  40. package/lib/models/user.model.d.ts.map +1 -1
  41. package/lib/models/video.model.d.ts +11 -0
  42. package/lib/models/video.model.d.ts.map +1 -1
  43. package/lib/schemas/videoPlan.schema.d.ts +88 -0
  44. package/lib/schemas/videoPlan.schema.d.ts.map +1 -1
  45. package/lib/schemas/videoPlan.schema.js +27 -1
  46. package/lib/services/agent/executor/visual.d.ts.map +1 -1
  47. package/lib/services/agent/executor/visual.js +40 -2
  48. package/lib/services/agent/mentionedRefs.d.ts.map +1 -1
  49. package/lib/services/agent/mentionedRefs.js +4 -1
  50. package/lib/services/agent/planner/structuralRules.d.ts.map +1 -1
  51. package/lib/services/agent/planner/structuralRules.js +8 -3
  52. package/lib/services/agent/poster/posterAgent.schema.d.ts +3 -3
  53. package/lib/services/agent/providerFallback/chains.d.ts +25 -0
  54. package/lib/services/agent/providerFallback/chains.d.ts.map +1 -1
  55. package/lib/services/agent/providerFallback/chains.js +59 -5
  56. package/lib/services/agent/repair/strategies/llmRepair.strategy.d.ts.map +1 -1
  57. package/lib/services/agent/repair/strategies/llmRepair.strategy.js +2 -0
  58. package/lib/services/agent/tools/animateImage.tool.d.ts +31 -0
  59. package/lib/services/agent/tools/animateImage.tool.d.ts.map +1 -1
  60. package/lib/services/agent/tools/animateImage.tool.js +10 -3
  61. package/lib/services/agent/tools/generateImage.tool.d.ts +8 -4
  62. package/lib/services/agent/tools/generateImage.tool.d.ts.map +1 -1
  63. package/lib/services/agent/tools/generateImage.tool.js +24 -7
  64. package/lib/services/agent/tools/generateVideo.tool.d.ts +32 -0
  65. package/lib/services/agent/tools/generateVideo.tool.d.ts.map +1 -1
  66. package/lib/services/agent/tools/generateVideo.tool.js +37 -9
  67. package/lib/services/agent/tools/planVideo.tool.d.ts +19 -0
  68. package/lib/services/agent/tools/planVideo.tool.d.ts.map +1 -1
  69. package/lib/services/aiGen/aiGenFactory.service.d.ts.map +1 -1
  70. package/lib/services/aiGen/aiGenFactory.service.js +40 -0
  71. package/lib/services/aiGen/canonicalAdapters/cameraControl.types.d.ts +6 -3
  72. package/lib/services/aiGen/canonicalAdapters/cameraControl.types.d.ts.map +1 -1
  73. package/lib/services/aiGen/canonicalAdapters/cameraControl.types.js +35 -0
  74. package/lib/services/aiGen/canonicalAdapters/cameraDirection.d.ts +36 -0
  75. package/lib/services/aiGen/canonicalAdapters/cameraDirection.d.ts.map +1 -0
  76. package/lib/services/aiGen/canonicalAdapters/cameraDirection.js +168 -0
  77. package/lib/services/aiGen/canonicalAdapters/index.d.ts +1 -0
  78. package/lib/services/aiGen/canonicalAdapters/index.d.ts.map +1 -1
  79. package/lib/services/aiGen/canonicalAdapters/index.js +1 -0
  80. package/lib/services/aiGen/canonicalAdapters/multiShot.types.d.ts +3 -2
  81. package/lib/services/aiGen/canonicalAdapters/multiShot.types.d.ts.map +1 -1
  82. package/lib/services/aiGen/helpers.d.ts +40 -0
  83. package/lib/services/aiGen/helpers.d.ts.map +1 -1
  84. package/lib/services/aiGen/helpers.js +48 -2
  85. package/lib/services/aiGen/index.d.ts +1 -0
  86. package/lib/services/aiGen/index.d.ts.map +1 -1
  87. package/lib/services/aiGen/index.js +3 -0
  88. package/lib/services/aiGen/providers/alibaba/alibaba.d.ts +13 -1
  89. package/lib/services/aiGen/providers/alibaba/alibaba.d.ts.map +1 -1
  90. package/lib/services/aiGen/providers/alibaba/alibaba.js +313 -33
  91. package/lib/services/aiGen/providers/bytedance/bytedance.service.d.ts +29 -1
  92. package/lib/services/aiGen/providers/bytedance/bytedance.service.d.ts.map +1 -1
  93. package/lib/services/aiGen/providers/bytedance/bytedance.service.js +371 -54
  94. package/lib/services/aiGen/providers/google/google.service.d.ts +16 -0
  95. package/lib/services/aiGen/providers/google/google.service.d.ts.map +1 -1
  96. package/lib/services/aiGen/providers/google/google.service.js +372 -116
  97. package/lib/services/aiGen/providers/google/googleKeyPool.d.ts +6 -0
  98. package/lib/services/aiGen/providers/google/googleKeyPool.d.ts.map +1 -1
  99. package/lib/services/aiGen/providers/google/googleKeyPool.js +8 -0
  100. package/lib/services/aiGen/providers/kling/kling.service.d.ts +17 -1
  101. package/lib/services/aiGen/providers/kling/kling.service.d.ts.map +1 -1
  102. package/lib/services/aiGen/providers/kling/kling.service.js +59 -3
  103. package/lib/services/aiGen/providers/kling/klingCredits.d.ts +1 -1
  104. package/lib/services/aiGen/providers/kling/klingCredits.d.ts.map +1 -1
  105. package/lib/services/aiGen/providers/kling/klingCredits.js +19 -1
  106. package/lib/services/aiGen/providers/kling/klingRequests.d.ts +37 -7
  107. package/lib/services/aiGen/providers/kling/klingRequests.d.ts.map +1 -1
  108. package/lib/services/aiGen/providers/kling/klingRequests.js +191 -13
  109. package/lib/services/aiGen/providers/kling/types.d.ts +78 -0
  110. package/lib/services/aiGen/providers/kling/types.d.ts.map +1 -1
  111. package/lib/services/aiGen/providers/minimax/minimax.service.d.ts +11 -2
  112. package/lib/services/aiGen/providers/minimax/minimax.service.d.ts.map +1 -1
  113. package/lib/services/aiGen/providers/minimax/minimax.service.js +168 -2
  114. package/lib/services/aiGen/providers/minimax/types.d.ts +63 -0
  115. package/lib/services/aiGen/providers/minimax/types.d.ts.map +1 -1
  116. package/lib/services/aiGen/providers/pixverse/pixverse.service.d.ts +12 -0
  117. package/lib/services/aiGen/providers/pixverse/pixverse.service.d.ts.map +1 -1
  118. package/lib/services/aiGen/providers/pixverse/pixverse.service.js +131 -17
  119. package/lib/services/aiGen/providers/runway/hostedModels.d.ts +84 -0
  120. package/lib/services/aiGen/providers/runway/hostedModels.d.ts.map +1 -0
  121. package/lib/services/aiGen/providers/runway/hostedModels.js +266 -0
  122. package/lib/services/aiGen/providers/runway/index.d.ts +1 -0
  123. package/lib/services/aiGen/providers/runway/index.d.ts.map +1 -1
  124. package/lib/services/aiGen/providers/runway/index.js +1 -0
  125. package/lib/services/aiGen/providers/runway/runway.service.d.ts +1 -1
  126. package/lib/services/aiGen/providers/runway/runway.service.d.ts.map +1 -1
  127. package/lib/services/aiGen/providers/runway/runway.service.js +118 -21
  128. package/lib/services/aiGen/providers/runway/types.d.ts +18 -3
  129. package/lib/services/aiGen/providers/runway/types.d.ts.map +1 -1
  130. package/lib/services/aiGen/providers/runway/types.js +31 -3
  131. package/lib/services/aiGen/providers/types.d.ts +55 -0
  132. package/lib/services/aiGen/providers/types.d.ts.map +1 -1
  133. package/lib/services/aiGen/providers/xai/xai.service.d.ts +19 -0
  134. package/lib/services/aiGen/providers/xai/xai.service.d.ts.map +1 -0
  135. package/lib/services/aiGen/providers/xai/xai.service.js +205 -0
  136. package/lib/services/firestore.service.d.ts +4 -0
  137. package/lib/services/firestore.service.d.ts.map +1 -1
  138. package/lib/services/firestore.service.js +5 -0
  139. package/lib/services/index.d.ts +2 -0
  140. package/lib/services/index.d.ts.map +1 -1
  141. package/lib/services/index.js +2 -0
  142. package/lib/services/klingElements/klingElementLibrary.service.d.ts +37 -0
  143. package/lib/services/klingElements/klingElementLibrary.service.d.ts.map +1 -0
  144. package/lib/services/klingElements/klingElementLibrary.service.js +179 -0
  145. package/lib/services/trialGuard/index.d.ts +2 -0
  146. package/lib/services/trialGuard/index.d.ts.map +1 -0
  147. package/lib/services/trialGuard/index.js +17 -0
  148. package/lib/services/trialGuard/trialSignals.d.ts +88 -0
  149. package/lib/services/trialGuard/trialSignals.d.ts.map +1 -0
  150. package/lib/services/trialGuard/trialSignals.js +160 -0
  151. package/lib/utils/helpers.d.ts.map +1 -1
  152. package/lib/utils/helpers.js +5 -1
  153. package/package.json +2 -1
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KlingElementLibrary = void 0;
4
+ const klingElement_model_1 = require("../../models/klingElement.model");
5
+ const logger_1 = require("../../utils/logger");
6
+ const errors_1 = require("../../utils/errors");
7
+ const kling_service_1 = require("../aiGen/providers/kling/kling.service");
8
+ const firestore_service_1 = require("../firestore.service");
9
+ const TAG = "[KlingElementLibrary]";
10
+ /** Preset list barely changes; one fetch per instance per 30 min is plenty. */
11
+ const PRESET_CACHE_MS = 30 * 60 * 1000;
12
+ /** Pending creation tasks refreshed per list call, to bound Kling calls. */
13
+ const MAX_REFRESH_PER_LIST = 10;
14
+ let presetCache;
15
+ function toPreset(e) {
16
+ return {
17
+ elementId: String(e.element_id),
18
+ name: e.element_name,
19
+ description: e.element_description,
20
+ referenceType: e.reference_type,
21
+ };
22
+ }
23
+ function invalid(message) {
24
+ throw new errors_1.UserFacingError(message, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
25
+ }
26
+ /**
27
+ * Per-user view over Kling's Element Library. Kling only knows our account,
28
+ * so ownership lives in Firestore (`klingElements`): a user sees, uses and
29
+ * deletes only elements they created, plus Kling's official presets.
30
+ */
31
+ class KlingElementLibrary {
32
+ static async create(userId, input) {
33
+ const name = input.name.trim();
34
+ const description = input.description.trim();
35
+ if (!name || name.length > klingElement_model_1.KLING_ELEMENT_LIMITS.nameMaxChars) {
36
+ invalid(`Element name must be 1–${klingElement_model_1.KLING_ELEMENT_LIMITS.nameMaxChars} characters.`);
37
+ }
38
+ if (!description || description.length > klingElement_model_1.KLING_ELEMENT_LIMITS.descriptionMaxChars) {
39
+ invalid(`Element description must be 1–${klingElement_model_1.KLING_ELEMENT_LIMITS.descriptionMaxChars} characters.`);
40
+ }
41
+ const referImageUrls = input.referImageUrls ?? [];
42
+ const isVideo = !!input.videoUrl;
43
+ if (isVideo === !!input.frontalImageUrl) {
44
+ invalid("Give either a front image (with 1–3 more angles) or one video, not both.");
45
+ }
46
+ if (!isVideo &&
47
+ (referImageUrls.length < klingElement_model_1.KLING_ELEMENT_LIMITS.minReferImages ||
48
+ referImageUrls.length > klingElement_model_1.KLING_ELEMENT_LIMITS.maxReferImages)) {
49
+ invalid(`An image element needs ${klingElement_model_1.KLING_ELEMENT_LIMITS.minReferImages}–${klingElement_model_1.KLING_ELEMENT_LIMITS.maxReferImages} images besides the front view.`);
50
+ }
51
+ // Creation is free to the user but may cost us Kling units — cap it.
52
+ const existing = await firestore_service_1.FirestoreService.getDataByKeyField(firestore_service_1.FirestoreService.klingElementsCol, "userId", "==", userId);
53
+ const live = existing.filter((r) => r.status !== "deleted" && r.status !== "failed").length;
54
+ if (live >= klingElement_model_1.KLING_ELEMENT_LIMITS.maxPerUser) {
55
+ invalid(`You can keep up to ${klingElement_model_1.KLING_ELEMENT_LIMITS.maxPerUser} elements. Delete one to add another.`);
56
+ }
57
+ const referenceType = isVideo ? "video_refer" : "image_refer";
58
+ const body = {
59
+ element_name: name,
60
+ element_description: description,
61
+ reference_type: referenceType,
62
+ ...(isVideo
63
+ ? { element_video_list: { refer_videos: [{ video_url: input.videoUrl }] } }
64
+ : {
65
+ element_image_list: {
66
+ frontal_image: input.frontalImageUrl,
67
+ refer_images: referImageUrls.map((image_url) => ({ image_url })),
68
+ },
69
+ }),
70
+ };
71
+ const taskId = await new kling_service_1.KlingService().createElement(body);
72
+ const now = new Date().toISOString();
73
+ const doc = {
74
+ userId,
75
+ taskId,
76
+ name,
77
+ description,
78
+ referenceType,
79
+ status: "submitted",
80
+ ...(isVideo ? { videoUrl: input.videoUrl } : { frontalImageUrl: input.frontalImageUrl, referImageUrls }),
81
+ createdAt: now,
82
+ updatedAt: now,
83
+ };
84
+ await firestore_service_1.FirestoreService.addData(firestore_service_1.FirestoreService.klingElementsCol, taskId, doc);
85
+ logger_1.logger.info(`${TAG} Element creation submitted`, { userId, taskId, referenceType });
86
+ return doc;
87
+ }
88
+ /** The user's elements (newest first), refreshing any still being created. */
89
+ static async listForUser(userId) {
90
+ // Equality-only query + in-memory sort: no composite index to deploy.
91
+ const rows = await firestore_service_1.FirestoreService.getDataByKeyField(firestore_service_1.FirestoreService.klingElementsCol, "userId", "==", userId);
92
+ const live = rows.filter((r) => r.status !== "deleted");
93
+ const pending = live.filter((r) => r.status === "submitted" || r.status === "processing");
94
+ const refreshed = new Map();
95
+ await Promise.all(pending.slice(0, MAX_REFRESH_PER_LIST).map(async (row) => {
96
+ const next = await this.refresh(row).catch((err) => {
97
+ logger_1.logger.warn(`${TAG} Element refresh failed`, {
98
+ taskId: row.taskId,
99
+ error: err instanceof Error ? err.message : String(err),
100
+ });
101
+ return row;
102
+ });
103
+ refreshed.set(row.taskId, next);
104
+ }));
105
+ return live
106
+ .map((r) => refreshed.get(r.taskId) ?? r)
107
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
108
+ }
109
+ /** Poll Kling for one creation task and persist the outcome. */
110
+ static async refresh(row) {
111
+ const task = await new kling_service_1.KlingService().getElementTask(row.taskId);
112
+ if (task.task_status === row.status)
113
+ return row;
114
+ const element = task.task_result?.elements?.[0];
115
+ const patch = {
116
+ status: task.task_status,
117
+ updatedAt: new Date().toISOString(),
118
+ ...(element ? { elementId: String(element.element_id) } : {}),
119
+ ...(task.task_status_msg ? { statusMessage: task.task_status_msg } : {}),
120
+ ...(task.final_unit_deduction ? { unitsCharged: task.final_unit_deduction } : {}),
121
+ };
122
+ await firestore_service_1.FirestoreService.updateData(firestore_service_1.FirestoreService.klingElementsCol, row.taskId, patch);
123
+ if (task.final_unit_deduction) {
124
+ // Element creation isn't on Kling's price list; log what it really costs.
125
+ logger_1.logger.info(`${TAG} Element creation charged`, {
126
+ taskId: row.taskId,
127
+ units: task.final_unit_deduction,
128
+ });
129
+ }
130
+ return { ...row, ...patch };
131
+ }
132
+ static async listPresets() {
133
+ if (presetCache && Date.now() - presetCache.at < PRESET_CACHE_MS)
134
+ return presetCache.presets;
135
+ const presets = (await new kling_service_1.KlingService().listPresetElements())
136
+ .filter((e) => e.status !== "deleted")
137
+ .map(toPreset);
138
+ presetCache = { at: Date.now(), presets };
139
+ return presets;
140
+ }
141
+ static async delete(userId, taskId) {
142
+ const row = await firestore_service_1.FirestoreService.getDataById(firestore_service_1.FirestoreService.klingElementsCol, taskId);
143
+ if (!row || row.userId !== userId)
144
+ invalid("Element not found.");
145
+ if (row.elementId && row.status === "succeed") {
146
+ await new kling_service_1.KlingService().deleteElement(row.elementId);
147
+ }
148
+ await firestore_service_1.FirestoreService.updateData(firestore_service_1.FirestoreService.klingElementsCol, taskId, {
149
+ status: "deleted",
150
+ updatedAt: new Date().toISOString(),
151
+ });
152
+ }
153
+ /**
154
+ * Resolve element ids from a generate request. Each must be a finished
155
+ * element the user owns or an official preset; anything else is rejected so
156
+ * one user can never reference another user's element.
157
+ */
158
+ static async resolveForGeneration(userId, elementIds) {
159
+ const ids = [...new Set(elementIds.map((id) => id.trim()).filter(Boolean))];
160
+ if (!ids.length)
161
+ return [];
162
+ const own = (await this.listForUser(userId)).filter((r) => r.status === "succeed" && r.elementId);
163
+ const ownById = new Map(own.map((r) => [r.elementId, r]));
164
+ const needPresets = ids.some((id) => !ownById.has(id));
165
+ const presetById = needPresets
166
+ ? new Map((await this.listPresets()).map((p) => [p.elementId, p]))
167
+ : new Map();
168
+ return ids.map((id) => {
169
+ const mine = ownById.get(id);
170
+ if (mine)
171
+ return { elementId: id, referenceType: mine.referenceType, name: mine.name };
172
+ const preset = presetById.get(id);
173
+ if (preset)
174
+ return { elementId: id, referenceType: preset.referenceType, name: preset.name };
175
+ return invalid(`Element ${id} isn't in your element library (or is still being created).`);
176
+ });
177
+ }
178
+ }
179
+ exports.KlingElementLibrary = KlingElementLibrary;
@@ -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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/utils/helpers.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAEnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,CAAC,MAAM,KAAK,CAAC;AAEpB,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG5C,eAAO,MAAM,eAAe,GAC1B,SAAS,MAAM,KACd,mBAAmB,GAAG,IASxB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,SAAS,MAAM,KAAG,kBAAkB,GAAG,IASlE,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,MAAM,mBAAmB,EACzB,OAAM,kBAA+C,WAgBtD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,MAAM,MAAM,EACZ,oBAAiB;AACjB;;;;;GAKG;AACH,QAAO,MAAgC,KACtC,MAQF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAC1B,UAAU,MAAM,EAChB,MAAM,MAAM,GAAG,SAAS,EACxB,UAAU,MAAM,KACf,MASF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,EAAE,OAAO,CAAC,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAInF,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,kBAAkB,GAAG,MAAM,CAEnF;AAOD,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,UAAU,GACX,MAAM,6CAA6C,CAAC;AAErD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,SAAQ,EACf,QAAQ,SAAM,oBAQf;AAGD,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,SAAS,EAAE,CAAC,CAAC;IACb,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAGxB,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AA0DrB,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAatE"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/utils/helpers.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAEnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,CAAC,MAAM,KAAK,CAAC;AAEpB,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG5C,eAAO,MAAM,eAAe,GAC1B,SAAS,MAAM,KACd,mBAAmB,GAAG,IASxB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,SAAS,MAAM,KAAG,kBAAkB,GAAG,IASlE,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,MAAM,mBAAmB,EACzB,OAAM,kBAA+C,WAgBtD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,MAAM,MAAM,EACZ,oBAAiB;AACjB;;;;;GAKG;AACH,QAAO,MAAgC,KACtC,MAYF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAC1B,UAAU,MAAM,EAChB,MAAM,MAAM,GAAG,SAAS,EACxB,UAAU,MAAM,KACf,MASF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,EAAE,OAAO,CAAC,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAInF,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,kBAAkB,GAAG,MAAM,CAEnF;AAOD,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,UAAU,GACX,MAAM,6CAA6C,CAAC;AAErD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,SAAQ,EACf,QAAQ,SAAM,oBAQf;AAGD,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,SAAS,EAAE,CAAC,CAAC;IACb,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAGxB,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AA0DrB,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAatE"}
@@ -62,7 +62,11 @@ floor = creditUnit_1.MIN_CREDITS_PER_PAID_OP) => {
62
62
  return 0;
63
63
  // Canonical rate (see globals/creditUnit.ts). USD_PER_CREDIT is cost basis;
64
64
  // the 2× user markup lives in plan pricing, not here.
65
- const credits = Math.ceil(cost / creditUnit_1.USD_PER_CREDIT);
65
+ // const credits = Math.ceil(cost / USD_PER_CREDIT); // REMOVED FOR v2 — ceil of float noise over-billed a credit
66
+ // Rounded to 6 dp first: summed per-second rates carry float noise
67
+ // ($1.00 + $1.40 = 2.4000000000000004 → 24.000000000000004 credits) that a
68
+ // bare ceil turned into 25. Real fractions of a credit still round up.
69
+ const credits = Math.ceil(Number((cost / creditUnit_1.USD_PER_CREDIT).toFixed(6)));
66
70
  return applyFloor ? Math.max(credits, floor) : credits;
67
71
  };
68
72
  exports.getCreditsFromCost = getCreditsFromCost;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidspotai-shared",
3
- "version": "1.0.124",
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",