autokap 1.3.31 → 1.4.2
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/assets/skill/SKILL.md +9 -0
- package/assets/skill/references/STANDARDS.md +236 -0
- package/dist/cli-contract.d.ts +1 -0
- package/dist/cli-contract.js +12 -1
- package/dist/cli-runner-local.d.ts +1 -0
- package/dist/cli-runner-local.js +4 -0
- package/dist/cli-runner.d.ts +2 -0
- package/dist/cli-runner.js +5 -0
- package/dist/cli.js +55 -2
- package/dist/crm/email-fallback.d.ts +16 -0
- package/dist/crm/email-fallback.js +217 -0
- package/dist/crm/run-campaign.d.ts +28 -0
- package/dist/crm/run-campaign.js +405 -0
- package/dist/crm/scrape-betalist.d.ts +20 -0
- package/dist/crm/scrape-betalist.js +194 -0
- package/dist/crm/scrape-landing.d.ts +24 -0
- package/dist/crm/scrape-landing.js +240 -0
- package/dist/crm/storage-upload.d.ts +14 -0
- package/dist/crm/storage-upload.js +40 -0
- package/dist/mockup.d.ts +7 -0
- package/dist/mockup.js +52 -6
- package/dist/opcode-runner.d.ts +2 -0
- package/dist/opcode-runner.js +4 -0
- package/dist/openrouter-tts.d.ts +6 -0
- package/dist/openrouter-tts.js +6 -2
- package/dist/types.d.ts +1 -1
- package/package.json +3 -2
- package/dist/server-capture-runtime.d.ts +0 -124
- package/dist/server-capture-runtime.js +0 -582
|
@@ -1,582 +0,0 @@
|
|
|
1
|
-
export class PlanLimitError extends Error {
|
|
2
|
-
status;
|
|
3
|
-
code;
|
|
4
|
-
constructor(message, status = 403, code = 'plan_limit') {
|
|
5
|
-
super(message);
|
|
6
|
-
this.name = 'PlanLimitError';
|
|
7
|
-
this.status = status;
|
|
8
|
-
this.code = code;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
export const SCREENSHOT_CREDIT_COST = 1;
|
|
12
|
-
const CREDIT_OVERAGE_CENTS = {
|
|
13
|
-
free: 0,
|
|
14
|
-
maker: 6,
|
|
15
|
-
team: 5,
|
|
16
|
-
};
|
|
17
|
-
const STRIPE_ACTIVE_STATUSES = new Set(['active', 'trialing', 'past_due']);
|
|
18
|
-
const BILLING_PLANS = [
|
|
19
|
-
{
|
|
20
|
-
id: 'free',
|
|
21
|
-
nameKey: 'free',
|
|
22
|
-
descriptionKey: 'forEvaluation',
|
|
23
|
-
monthlyPriceEur: 0,
|
|
24
|
-
yearlyPriceEur: 0,
|
|
25
|
-
previewCurrent: true,
|
|
26
|
-
highlights: ['credits', 'projects', 'devLinks', 'retention', 'support'],
|
|
27
|
-
entitlements: {
|
|
28
|
-
creditsPerMonth: 25,
|
|
29
|
-
maxProjects: 1,
|
|
30
|
-
maxPresetsPerProject: 2,
|
|
31
|
-
allowFullPageCapture: true,
|
|
32
|
-
allowElementCapture: true,
|
|
33
|
-
retentionDays: 7,
|
|
34
|
-
watermarkScreenshots: false,
|
|
35
|
-
maxLanguages: null,
|
|
36
|
-
maxThemes: null,
|
|
37
|
-
apiAccess: true,
|
|
38
|
-
captureCompleteWebhook: false,
|
|
39
|
-
priorityQueue: false,
|
|
40
|
-
maxParallelCaptures: 1,
|
|
41
|
-
teamMembers: false,
|
|
42
|
-
aiDailyCostLimitUsd: 0.05,
|
|
43
|
-
apiRateLimitRpm: 10,
|
|
44
|
-
maxDevLinks: 1,
|
|
45
|
-
maxTeamMembersPerProject: null,
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
id: 'maker',
|
|
50
|
-
nameKey: 'maker',
|
|
51
|
-
descriptionKey: 'forMakers',
|
|
52
|
-
monthlyPriceEur: 15,
|
|
53
|
-
yearlyPriceEur: 144,
|
|
54
|
-
highlighted: true,
|
|
55
|
-
highlights: ['credits', 'projects', 'devLinks', 'retention', 'parallelCaptures', 'captureCompleteWebhook', 'support'],
|
|
56
|
-
entitlements: {
|
|
57
|
-
creditsPerMonth: 100,
|
|
58
|
-
maxProjects: 3,
|
|
59
|
-
maxPresetsPerProject: 5,
|
|
60
|
-
allowFullPageCapture: true,
|
|
61
|
-
allowElementCapture: true,
|
|
62
|
-
retentionDays: 30,
|
|
63
|
-
watermarkScreenshots: false,
|
|
64
|
-
maxLanguages: null,
|
|
65
|
-
maxThemes: null,
|
|
66
|
-
apiAccess: true,
|
|
67
|
-
captureCompleteWebhook: true,
|
|
68
|
-
priorityQueue: false,
|
|
69
|
-
maxParallelCaptures: 2,
|
|
70
|
-
teamMembers: false,
|
|
71
|
-
aiDailyCostLimitUsd: 0.10,
|
|
72
|
-
apiRateLimitRpm: 60,
|
|
73
|
-
maxDevLinks: 10,
|
|
74
|
-
maxTeamMembersPerProject: null,
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
id: 'team',
|
|
79
|
-
nameKey: 'team',
|
|
80
|
-
descriptionKey: 'forTeams',
|
|
81
|
-
monthlyPriceEur: 49,
|
|
82
|
-
yearlyPriceEur: 468,
|
|
83
|
-
highlights: ['credits', 'projects', 'devLinks', 'retention', 'parallelCaptures', 'teamMembers', 'priorityQueue', 'support'],
|
|
84
|
-
entitlements: {
|
|
85
|
-
creditsPerMonth: 500,
|
|
86
|
-
maxProjects: null,
|
|
87
|
-
maxPresetsPerProject: null,
|
|
88
|
-
allowFullPageCapture: true,
|
|
89
|
-
allowElementCapture: true,
|
|
90
|
-
retentionDays: 90,
|
|
91
|
-
watermarkScreenshots: false,
|
|
92
|
-
maxLanguages: null,
|
|
93
|
-
maxThemes: null,
|
|
94
|
-
apiAccess: true,
|
|
95
|
-
captureCompleteWebhook: true,
|
|
96
|
-
priorityQueue: true,
|
|
97
|
-
maxParallelCaptures: 5,
|
|
98
|
-
teamMembers: true,
|
|
99
|
-
aiDailyCostLimitUsd: 0.15,
|
|
100
|
-
apiRateLimitRpm: 200,
|
|
101
|
-
maxDevLinks: null,
|
|
102
|
-
maxTeamMembersPerProject: 5,
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
];
|
|
106
|
-
export function getBillingPlan(planId) {
|
|
107
|
-
return BILLING_PLANS.find((plan) => plan.id === planId) ?? BILLING_PLANS[0];
|
|
108
|
-
}
|
|
109
|
-
export function coerceBillingPlanId(value) {
|
|
110
|
-
if (typeof value !== 'string')
|
|
111
|
-
return null;
|
|
112
|
-
switch (value) {
|
|
113
|
-
case 'free':
|
|
114
|
-
case 'maker':
|
|
115
|
-
case 'team':
|
|
116
|
-
return value;
|
|
117
|
-
default:
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function getServerDefaultBillingPlanId() {
|
|
122
|
-
return (coerceBillingPlanId(process.env.DEFAULT_BILLING_PLAN)
|
|
123
|
-
?? coerceBillingPlanId(process.env.NEXT_PUBLIC_DEFAULT_BILLING_PLAN)
|
|
124
|
-
?? 'free');
|
|
125
|
-
}
|
|
126
|
-
function resolvePlanFromBillingAccount(account, defaultPlanId) {
|
|
127
|
-
const adminPlanId = coerceBillingPlanId(account?.admin_override_plan_id);
|
|
128
|
-
if (adminPlanId) {
|
|
129
|
-
return { planId: adminPlanId, source: 'admin_override' };
|
|
130
|
-
}
|
|
131
|
-
const stripePlanId = coerceBillingPlanId(account?.stripe_plan_id);
|
|
132
|
-
if (stripePlanId && shouldUseStripePlan(account?.stripe_subscription_status)) {
|
|
133
|
-
return { planId: stripePlanId, source: 'stripe' };
|
|
134
|
-
}
|
|
135
|
-
return { planId: defaultPlanId, source: 'default' };
|
|
136
|
-
}
|
|
137
|
-
export async function getBillingAccountForUser(supabase, userId) {
|
|
138
|
-
const { data, error } = await supabase
|
|
139
|
-
.from('billing_accounts')
|
|
140
|
-
.select('*')
|
|
141
|
-
.eq('user_id', userId)
|
|
142
|
-
.maybeSingle();
|
|
143
|
-
if (error) {
|
|
144
|
-
if (error.code === '42P01') {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
throw new Error(error.message);
|
|
148
|
-
}
|
|
149
|
-
return data ?? null;
|
|
150
|
-
}
|
|
151
|
-
async function getResolvedBillingPlanForUserId(supabase, userId) {
|
|
152
|
-
const account = await getBillingAccountForUser(supabase, userId);
|
|
153
|
-
const resolved = resolvePlanFromBillingAccount(account, getServerDefaultBillingPlanId());
|
|
154
|
-
return getBillingPlan(resolved.planId);
|
|
155
|
-
}
|
|
156
|
-
export async function getProjectOwnerBillingContext(supabase, projectId) {
|
|
157
|
-
const { data, error } = await supabase
|
|
158
|
-
.from('projects')
|
|
159
|
-
.select('user_id')
|
|
160
|
-
.eq('id', projectId)
|
|
161
|
-
.single();
|
|
162
|
-
if (error || !data?.user_id) {
|
|
163
|
-
throw new Error('Project not found');
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
ownerUserId: data.user_id,
|
|
167
|
-
plan: await getResolvedBillingPlanForUserId(supabase, data.user_id),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
export function shouldUseStripePlan(status) {
|
|
171
|
-
return !!status && STRIPE_ACTIVE_STATUSES.has(status);
|
|
172
|
-
}
|
|
173
|
-
export function isYearlySubscription(account) {
|
|
174
|
-
if (!account?.stripe_current_period_start || !account?.stripe_current_period_end) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
const start = new Date(account.stripe_current_period_start).getTime();
|
|
178
|
-
const end = new Date(account.stripe_current_period_end).getTime();
|
|
179
|
-
return (end - start) / (1000 * 60 * 60 * 24) > 60;
|
|
180
|
-
}
|
|
181
|
-
export function getStripeOveragePriceIdForPlan(planId) {
|
|
182
|
-
switch (planId) {
|
|
183
|
-
case 'maker':
|
|
184
|
-
return process.env.STRIPE_PRICE_ID_MAKER_OVERAGE ?? null;
|
|
185
|
-
case 'team':
|
|
186
|
-
return process.env.STRIPE_PRICE_ID_TEAM_OVERAGE ?? null;
|
|
187
|
-
case 'free':
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
function getStripeSecretKey() {
|
|
192
|
-
const key = process.env.STRIPE_SECRET_KEY;
|
|
193
|
-
if (!key) {
|
|
194
|
-
throw new Error('Missing STRIPE_SECRET_KEY');
|
|
195
|
-
}
|
|
196
|
-
return key;
|
|
197
|
-
}
|
|
198
|
-
function getStripeMeterEventName() {
|
|
199
|
-
return process.env.STRIPE_METER_EVENT_NAME ?? 'credit';
|
|
200
|
-
}
|
|
201
|
-
async function stripeRequest(path, body, method) {
|
|
202
|
-
const resolvedMethod = method ?? (body ? 'POST' : 'GET');
|
|
203
|
-
const response = await fetch(`https://api.stripe.com${path}`, {
|
|
204
|
-
method: resolvedMethod,
|
|
205
|
-
headers: {
|
|
206
|
-
Authorization: `Bearer ${getStripeSecretKey()}`,
|
|
207
|
-
...(body ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {}),
|
|
208
|
-
},
|
|
209
|
-
body: body?.toString(),
|
|
210
|
-
});
|
|
211
|
-
const data = (await response.json());
|
|
212
|
-
if (!response.ok) {
|
|
213
|
-
throw new Error(data.error?.message || `Stripe request failed: ${path}`);
|
|
214
|
-
}
|
|
215
|
-
return data;
|
|
216
|
-
}
|
|
217
|
-
export async function recordStripeMeterEvent(params) {
|
|
218
|
-
const body = new URLSearchParams();
|
|
219
|
-
body.set('event_name', getStripeMeterEventName());
|
|
220
|
-
body.set('payload[stripe_customer_id]', params.customerId);
|
|
221
|
-
body.set('payload[value]', String(params.value));
|
|
222
|
-
body.set('identifier', params.identifier.slice(0, 100));
|
|
223
|
-
body.set('timestamp', String(Math.floor(Date.now() / 1000)));
|
|
224
|
-
await stripeRequest('/v1/billing/meter_events', body);
|
|
225
|
-
}
|
|
226
|
-
export function getCaptureConfigLimitViolations(config, entitlements) {
|
|
227
|
-
const violations = [];
|
|
228
|
-
if (entitlements.maxLanguages !== null
|
|
229
|
-
&& config.langs.length > entitlements.maxLanguages) {
|
|
230
|
-
violations.push(`This plan allows up to ${entitlements.maxLanguages} language${entitlements.maxLanguages > 1 ? 's' : ''}.`);
|
|
231
|
-
}
|
|
232
|
-
if (entitlements.maxThemes !== null
|
|
233
|
-
&& config.themes.length > entitlements.maxThemes) {
|
|
234
|
-
violations.push(`This plan allows up to ${entitlements.maxThemes} theme${entitlements.maxThemes > 1 ? 's' : ''}.`);
|
|
235
|
-
}
|
|
236
|
-
if ((config.elements?.length ?? 0) > 0 && !entitlements.allowElementCapture) {
|
|
237
|
-
violations.push('Element captures are not included in this plan.');
|
|
238
|
-
}
|
|
239
|
-
return violations;
|
|
240
|
-
}
|
|
241
|
-
export function ensureCaptureConfigAllowed(plan, config) {
|
|
242
|
-
const violations = getCaptureConfigLimitViolations(config, plan.entitlements);
|
|
243
|
-
if (violations.length > 0) {
|
|
244
|
-
throw new PlanLimitError(violations[0], 403, 'capture_feature_unavailable');
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
function sortByMostRecent(items) {
|
|
248
|
-
return [...items].sort((left, right) => {
|
|
249
|
-
const diff = new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime();
|
|
250
|
-
if (diff !== 0)
|
|
251
|
-
return diff;
|
|
252
|
-
return left.id.localeCompare(right.id);
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
function computeLockedProjectIds(projects, maxProjects) {
|
|
256
|
-
if (maxProjects === null || projects.length <= maxProjects) {
|
|
257
|
-
return new Set();
|
|
258
|
-
}
|
|
259
|
-
return new Set(sortByMostRecent(projects).slice(maxProjects).map((project) => project.id));
|
|
260
|
-
}
|
|
261
|
-
function computeLockedPresetIds(presets, maxPresetsPerProject) {
|
|
262
|
-
if (maxPresetsPerProject === null || presets.length <= maxPresetsPerProject) {
|
|
263
|
-
return new Set();
|
|
264
|
-
}
|
|
265
|
-
return new Set(sortByMostRecent(presets).slice(maxPresetsPerProject).map((preset) => preset.id));
|
|
266
|
-
}
|
|
267
|
-
export async function getLockedResourceIds(supabase, ownerUserId, plan) {
|
|
268
|
-
const { data: projects } = await supabase
|
|
269
|
-
.from('projects')
|
|
270
|
-
.select('id, updated_at')
|
|
271
|
-
.eq('user_id', ownerUserId)
|
|
272
|
-
.order('updated_at', { ascending: false });
|
|
273
|
-
const projectList = (projects ?? []);
|
|
274
|
-
const lockedProjectIds = computeLockedProjectIds(projectList, plan.entitlements.maxProjects);
|
|
275
|
-
const lockedPresetIdsByProject = new Map();
|
|
276
|
-
if (plan.entitlements.maxPresetsPerProject !== null) {
|
|
277
|
-
const activeProjectIds = projectList
|
|
278
|
-
.map((project) => project.id)
|
|
279
|
-
.filter((id) => !lockedProjectIds.has(id));
|
|
280
|
-
if (activeProjectIds.length > 0) {
|
|
281
|
-
const { data: presets } = await supabase
|
|
282
|
-
.from('presets')
|
|
283
|
-
.select('id, project_id, updated_at')
|
|
284
|
-
.in('project_id', activeProjectIds)
|
|
285
|
-
.order('updated_at', { ascending: false });
|
|
286
|
-
const presetsByProject = new Map();
|
|
287
|
-
for (const preset of (presets ?? [])) {
|
|
288
|
-
const list = presetsByProject.get(preset.project_id) ?? [];
|
|
289
|
-
list.push({ id: preset.id, updated_at: preset.updated_at });
|
|
290
|
-
presetsByProject.set(preset.project_id, list);
|
|
291
|
-
}
|
|
292
|
-
for (const [projectId, projectPresets] of presetsByProject.entries()) {
|
|
293
|
-
const locked = computeLockedPresetIds(projectPresets, plan.entitlements.maxPresetsPerProject);
|
|
294
|
-
if (locked.size > 0) {
|
|
295
|
-
lockedPresetIdsByProject.set(projectId, locked);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return { lockedProjectIds, lockedPresetIdsByProject };
|
|
301
|
-
}
|
|
302
|
-
export function ensureResourceNotLocked(resourceId, lockedIds, resourceType = 'preset') {
|
|
303
|
-
if (lockedIds.has(resourceId)) {
|
|
304
|
-
throw new PlanLimitError(`This ${resourceType} is locked because it exceeds your current plan limits. Upgrade or remove other ${resourceType}s to regain access.`, 403, 'resource_locked');
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
function getCreditOverageRateCents(planId) {
|
|
308
|
-
return CREDIT_OVERAGE_CENTS[planId];
|
|
309
|
-
}
|
|
310
|
-
function getCurrentOverageCredits(params) {
|
|
311
|
-
return Math.max(0, params.usedCredits - params.quota);
|
|
312
|
-
}
|
|
313
|
-
function getCurrentOverageSpendCents(params) {
|
|
314
|
-
return getCurrentOverageCredits({
|
|
315
|
-
quota: params.quota,
|
|
316
|
-
usedCredits: params.usedCredits,
|
|
317
|
-
}) * getCreditOverageRateCents(params.planId);
|
|
318
|
-
}
|
|
319
|
-
function getProjectedOverageSpendCents(params) {
|
|
320
|
-
return getCurrentOverageSpendCents({
|
|
321
|
-
planId: params.planId,
|
|
322
|
-
quota: params.quota,
|
|
323
|
-
usedCredits: params.usedCreditsBefore + params.requestedCredits,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
function getRemainingOverageBudgetCents(params) {
|
|
327
|
-
if (params.overageLimitCents === null) {
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
return Math.max(0, params.overageLimitCents - getCurrentOverageSpendCents({
|
|
331
|
-
planId: params.planId,
|
|
332
|
-
quota: params.quota,
|
|
333
|
-
usedCredits: params.usedCredits,
|
|
334
|
-
}));
|
|
335
|
-
}
|
|
336
|
-
export function getRemainingOverageCredits(params) {
|
|
337
|
-
if (params.overageLimitCents === null) {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
const rateCents = getCreditOverageRateCents(params.planId);
|
|
341
|
-
if (rateCents <= 0) {
|
|
342
|
-
return 0;
|
|
343
|
-
}
|
|
344
|
-
const remainingBudgetCents = getRemainingOverageBudgetCents(params) ?? 0;
|
|
345
|
-
return Math.floor(remainingBudgetCents / rateCents);
|
|
346
|
-
}
|
|
347
|
-
export function getCaptureOverageState(params) {
|
|
348
|
-
const spendCents = getCurrentOverageSpendCents({
|
|
349
|
-
planId: params.planId,
|
|
350
|
-
quota: params.quota,
|
|
351
|
-
usedCredits: params.usedCredits,
|
|
352
|
-
});
|
|
353
|
-
const remainingBudgetCents = getRemainingOverageBudgetCents({
|
|
354
|
-
planId: params.planId,
|
|
355
|
-
quota: params.quota,
|
|
356
|
-
usedCredits: params.usedCredits,
|
|
357
|
-
overageLimitCents: params.overageLimitCents,
|
|
358
|
-
});
|
|
359
|
-
const remainingCredits = getRemainingOverageCredits({
|
|
360
|
-
planId: params.planId,
|
|
361
|
-
quota: params.quota,
|
|
362
|
-
usedCredits: params.usedCredits,
|
|
363
|
-
overageLimitCents: params.overageLimitCents,
|
|
364
|
-
});
|
|
365
|
-
const eligible = params.planId !== 'free'
|
|
366
|
-
&& params.allowOverages
|
|
367
|
-
&& params.hasActiveSubscription
|
|
368
|
-
&& !params.isYearlySubscription
|
|
369
|
-
&& params.hasOveragePrice;
|
|
370
|
-
const limitReached = eligible
|
|
371
|
-
&& params.overageLimitCents !== null
|
|
372
|
-
&& (remainingBudgetCents ?? 0) <= 0;
|
|
373
|
-
return {
|
|
374
|
-
eligible,
|
|
375
|
-
enabled: eligible && !limitReached,
|
|
376
|
-
limitReached,
|
|
377
|
-
spendCents,
|
|
378
|
-
remainingBudgetCents,
|
|
379
|
-
remainingCredits,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
export function getSignupBonusCredits(account, billingPeriodStart) {
|
|
383
|
-
if (!account?.signup_bonus_credits || !account.created_at)
|
|
384
|
-
return 0;
|
|
385
|
-
const created = new Date(account.created_at);
|
|
386
|
-
const periodStart = new Date(billingPeriodStart);
|
|
387
|
-
return created >= periodStart ? account.signup_bonus_credits : 0;
|
|
388
|
-
}
|
|
389
|
-
export function ensureMonthlyCreditsQuota(plan, usedCredits, requestedCredits, allowOverage = false, overageLimitCents = null, bonusCredits = 0) {
|
|
390
|
-
const effectiveQuota = plan.entitlements.creditsPerMonth + bonusCredits;
|
|
391
|
-
if (allowOverage) {
|
|
392
|
-
if (requestedCredits > 2500) {
|
|
393
|
-
throw new PlanLimitError(`Batch too large: ${requestedCredits} credits requested. Maximum 2500 per request.`, 400, 'batch_too_large');
|
|
394
|
-
}
|
|
395
|
-
if (overageLimitCents !== null) {
|
|
396
|
-
const projectedOverageSpendCents = getProjectedOverageSpendCents({
|
|
397
|
-
planId: plan.id,
|
|
398
|
-
quota: effectiveQuota,
|
|
399
|
-
usedCreditsBefore: usedCredits,
|
|
400
|
-
requestedCredits,
|
|
401
|
-
});
|
|
402
|
-
if (projectedOverageSpendCents > overageLimitCents) {
|
|
403
|
-
throw new PlanLimitError(`Overage limit exceeded: projected overage spend is €${(projectedOverageSpendCents / 100).toFixed(2)} for a €${(overageLimitCents / 100).toFixed(2)} cap.`, 403, 'credit_overage_limit_exceeded');
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
if (usedCredits + requestedCredits > effectiveQuota) {
|
|
409
|
-
throw new PlanLimitError(`Monthly credit quota exceeded: ${usedCredits} of ${effectiveQuota} used, ${requestedCredits} requested. Upgrade or wait until next month.`, 403, 'credit_quota_exceeded');
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
async function resolveStripeBillingPeriodStart(supabase, userId) {
|
|
413
|
-
const { data: account } = await supabase
|
|
414
|
-
.from('billing_accounts')
|
|
415
|
-
.select('stripe_current_period_start, stripe_current_period_end')
|
|
416
|
-
.eq('user_id', userId)
|
|
417
|
-
.maybeSingle();
|
|
418
|
-
if (!account?.stripe_current_period_start) {
|
|
419
|
-
return new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), 1)).toISOString();
|
|
420
|
-
}
|
|
421
|
-
const periodStart = new Date(account.stripe_current_period_start);
|
|
422
|
-
const periodEnd = account.stripe_current_period_end
|
|
423
|
-
? new Date(account.stripe_current_period_end)
|
|
424
|
-
: null;
|
|
425
|
-
const isYearly = periodEnd
|
|
426
|
-
&& (periodEnd.getTime() - periodStart.getTime()) / (1000 * 60 * 60 * 24) > 60;
|
|
427
|
-
if (!isYearly) {
|
|
428
|
-
return periodStart.toISOString();
|
|
429
|
-
}
|
|
430
|
-
const now = new Date();
|
|
431
|
-
const dayOfMonth = periodStart.getUTCDate();
|
|
432
|
-
let resetDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), dayOfMonth));
|
|
433
|
-
if (resetDate > now) {
|
|
434
|
-
resetDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, dayOfMonth));
|
|
435
|
-
}
|
|
436
|
-
if (resetDate < periodStart) {
|
|
437
|
-
resetDate = periodStart;
|
|
438
|
-
}
|
|
439
|
-
return resetDate.toISOString();
|
|
440
|
-
}
|
|
441
|
-
export async function getOwnerBillingUsage(supabase, ownerUserId, billingPeriodStartOverride) {
|
|
442
|
-
const [{ data: ownerProjects, error: ownerProjectsError }, billingPeriodStart] = await Promise.all([
|
|
443
|
-
supabase.from('projects').select('id').eq('user_id', ownerUserId),
|
|
444
|
-
billingPeriodStartOverride
|
|
445
|
-
? Promise.resolve(billingPeriodStartOverride)
|
|
446
|
-
: resolveStripeBillingPeriodStart(supabase, ownerUserId),
|
|
447
|
-
]);
|
|
448
|
-
if (ownerProjectsError) {
|
|
449
|
-
throw new Error(ownerProjectsError.message);
|
|
450
|
-
}
|
|
451
|
-
const projectIds = (ownerProjects ?? []).map((project) => project.id);
|
|
452
|
-
if (projectIds.length === 0) {
|
|
453
|
-
return { billingPeriodStart, credits: 0 };
|
|
454
|
-
}
|
|
455
|
-
const { data: usageRows, error: usageError } = await supabase
|
|
456
|
-
.from('credit_usage')
|
|
457
|
-
.select('type, credits')
|
|
458
|
-
.in('project_id', projectIds)
|
|
459
|
-
.gte('created_at', billingPeriodStart);
|
|
460
|
-
if (usageError) {
|
|
461
|
-
throw new Error(usageError.message);
|
|
462
|
-
}
|
|
463
|
-
let credits = 0;
|
|
464
|
-
for (const row of usageRows ?? []) {
|
|
465
|
-
credits += Number(row.credits ?? 0);
|
|
466
|
-
}
|
|
467
|
-
return { billingPeriodStart, credits };
|
|
468
|
-
}
|
|
469
|
-
export function getIncrementalOverageCount(params) {
|
|
470
|
-
const overageBefore = Math.max(0, params.usedBefore - params.quota);
|
|
471
|
-
const overageAfter = Math.max(0, params.usedBefore + params.completedCount - params.quota);
|
|
472
|
-
return Math.max(0, overageAfter - overageBefore);
|
|
473
|
-
}
|
|
474
|
-
function getRetentionCutoffIso(retentionDays, now = new Date()) {
|
|
475
|
-
return new Date(now.getTime() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
|
|
476
|
-
}
|
|
477
|
-
function extractStoragePathFromPublicUrl(url, bucket = 'screenshots') {
|
|
478
|
-
if (!url)
|
|
479
|
-
return null;
|
|
480
|
-
const marker = `/storage/v1/object/public/${bucket}/`;
|
|
481
|
-
const index = url.indexOf(marker);
|
|
482
|
-
if (index === -1)
|
|
483
|
-
return null;
|
|
484
|
-
return decodeURIComponent(url.slice(index + marker.length));
|
|
485
|
-
}
|
|
486
|
-
async function removeStorageObjects(supabase, bucket, paths) {
|
|
487
|
-
if (paths.size === 0)
|
|
488
|
-
return;
|
|
489
|
-
await Promise.all(Array.from(paths).map(async (path) => {
|
|
490
|
-
try {
|
|
491
|
-
await supabase.storage.from(bucket).remove([path]);
|
|
492
|
-
}
|
|
493
|
-
catch {
|
|
494
|
-
// Best effort.
|
|
495
|
-
}
|
|
496
|
-
}));
|
|
497
|
-
}
|
|
498
|
-
async function removeCaptureAssets(supabase, path) {
|
|
499
|
-
if (path.startsWith('cli/')) {
|
|
500
|
-
await supabase.storage.from('screenshots').remove([path]);
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
const parts = path.split('/');
|
|
504
|
-
const folder = parts.slice(0, -1).join('/');
|
|
505
|
-
if (!folder) {
|
|
506
|
-
await supabase.storage.from('screenshots').remove([path]);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
const listPrefix = parts.slice(0, -2).join('/');
|
|
510
|
-
const folderName = parts.at(-2);
|
|
511
|
-
if (!folderName) {
|
|
512
|
-
await supabase.storage.from('screenshots').remove([path]);
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
const { data } = await supabase.storage.from('screenshots').list(listPrefix, { limit: 100 });
|
|
516
|
-
const targetFolder = data?.find((entry) => entry.name === folderName);
|
|
517
|
-
if (!targetFolder) {
|
|
518
|
-
await supabase.storage.from('screenshots').remove([path]);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
const { data: files } = await supabase.storage.from('screenshots').list(folder, { limit: 100 });
|
|
522
|
-
const paths = (files ?? []).map((file) => `${folder}/${file.name}`);
|
|
523
|
-
if (paths.length === 0) {
|
|
524
|
-
await supabase.storage.from('screenshots').remove([path]);
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
await supabase.storage.from('screenshots').remove(paths);
|
|
528
|
-
}
|
|
529
|
-
async function getProjectIdsForOwner(supabase, ownerUserId) {
|
|
530
|
-
const { data: projects, error } = await supabase
|
|
531
|
-
.from('projects')
|
|
532
|
-
.select('id')
|
|
533
|
-
.eq('user_id', ownerUserId);
|
|
534
|
-
if (error) {
|
|
535
|
-
throw new Error(error.message);
|
|
536
|
-
}
|
|
537
|
-
return (projects ?? []).map((project) => project.id);
|
|
538
|
-
}
|
|
539
|
-
export async function cleanupExpiredCapturesForOwner(supabase, ownerUserId) {
|
|
540
|
-
const plan = await getResolvedBillingPlanForUserId(supabase, ownerUserId);
|
|
541
|
-
const retentionCutoff = getRetentionCutoffIso(plan.entitlements.retentionDays);
|
|
542
|
-
const projectIds = await getProjectIdsForOwner(supabase, ownerUserId);
|
|
543
|
-
if (projectIds.length === 0) {
|
|
544
|
-
return { deletedCaptures: 0 };
|
|
545
|
-
}
|
|
546
|
-
const { data: captures, error: capturesError } = await supabase
|
|
547
|
-
.from('captures')
|
|
548
|
-
.select('id, screenshot_url')
|
|
549
|
-
.in('project_id', projectIds)
|
|
550
|
-
.lt('created_at', retentionCutoff);
|
|
551
|
-
if (capturesError) {
|
|
552
|
-
throw new Error(capturesError.message);
|
|
553
|
-
}
|
|
554
|
-
const rows = captures ?? [];
|
|
555
|
-
if (rows.length === 0) {
|
|
556
|
-
return { deletedCaptures: 0 };
|
|
557
|
-
}
|
|
558
|
-
const uniquePaths = new Set();
|
|
559
|
-
for (const row of rows) {
|
|
560
|
-
const path = extractStoragePathFromPublicUrl(row.screenshot_url);
|
|
561
|
-
if (path) {
|
|
562
|
-
uniquePaths.add(path);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
await Promise.all(Array.from(uniquePaths).map(async (path) => {
|
|
566
|
-
try {
|
|
567
|
-
await removeCaptureAssets(supabase, path);
|
|
568
|
-
}
|
|
569
|
-
catch {
|
|
570
|
-
// Best effort.
|
|
571
|
-
}
|
|
572
|
-
}));
|
|
573
|
-
const { error: deleteError } = await supabase
|
|
574
|
-
.from('captures')
|
|
575
|
-
.delete()
|
|
576
|
-
.in('id', rows.map((row) => row.id));
|
|
577
|
-
if (deleteError) {
|
|
578
|
-
throw new Error(deleteError.message);
|
|
579
|
-
}
|
|
580
|
-
return { deletedCaptures: rows.length };
|
|
581
|
-
}
|
|
582
|
-
//# sourceMappingURL=server-capture-runtime.js.map
|