@donotdev/functions 0.0.12 → 0.0.13
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/package.json +4 -4
- package/src/firebase/auth/setCustomClaims.ts +19 -5
- package/src/firebase/baseFunction.ts +11 -3
- package/src/firebase/billing/changePlan.ts +5 -1
- package/src/firebase/billing/createCustomerPortal.ts +6 -2
- package/src/firebase/billing/webhookHandler.ts +4 -1
- package/src/firebase/crud/aggregate.ts +5 -1
- package/src/firebase/crud/create.ts +17 -4
- package/src/firebase/crud/list.ts +9 -4
- package/src/firebase/crud/update.ts +17 -4
- package/src/firebase/oauth/exchangeToken.ts +17 -4
- package/src/shared/__tests__/validation.test.ts +5 -3
- package/src/shared/billing/__tests__/createCheckout.test.ts +123 -22
- package/src/shared/billing/__tests__/webhookHandler.test.ts +121 -30
- package/src/shared/firebase.ts +1 -1
- package/src/shared/logger.ts +7 -1
- package/src/shared/oauth/__tests__/exchangeToken.test.ts +6 -8
- package/src/shared/oauth/__tests__/grantAccess.test.ts +31 -14
- package/src/shared/utils/internal/auth.ts +10 -3
- package/src/shared/utils/internal/rateLimiter.ts +8 -2
- package/src/shared/utils.ts +5 -1
- package/src/supabase/auth/deleteAccount.ts +1 -1
- package/src/supabase/auth/getCustomClaims.ts +5 -3
- package/src/supabase/auth/getUserAuthStatus.ts +5 -3
- package/src/supabase/auth/removeCustomClaims.ts +10 -5
- package/src/supabase/auth/setCustomClaims.ts +9 -4
- package/src/supabase/baseFunction.ts +77 -22
- package/src/supabase/billing/cancelSubscription.ts +9 -3
- package/src/supabase/billing/changePlan.ts +20 -5
- package/src/supabase/billing/createCheckoutSession.ts +20 -5
- package/src/supabase/billing/createCustomerPortal.ts +14 -4
- package/src/supabase/billing/refreshSubscriptionStatus.ts +29 -9
- package/src/supabase/crud/aggregate.ts +14 -4
- package/src/supabase/crud/create.ts +30 -11
- package/src/supabase/crud/delete.ts +11 -3
- package/src/supabase/crud/get.ts +25 -3
- package/src/supabase/crud/list.ts +76 -22
- package/src/supabase/crud/update.ts +32 -10
- package/src/supabase/helpers/authProvider.ts +5 -2
- package/src/supabase/index.ts +1 -4
- package/src/supabase/registerCrudFunctions.ts +11 -9
- package/src/supabase/utils/idempotency.ts +13 -15
- package/src/supabase/utils/monitoring.ts +5 -1
- package/src/supabase/utils/rateLimiter.ts +13 -3
- package/src/vercel/api/billing/webhook-handler.ts +6 -2
- package/src/vercel/api/crud/create.ts +7 -2
- package/src/vercel/api/crud/delete.ts +3 -1
- package/src/vercel/api/crud/get.ts +3 -1
- package/src/vercel/api/crud/list.ts +3 -1
- package/src/vercel/api/crud/update.ts +7 -2
|
@@ -22,7 +22,11 @@ import type {
|
|
|
22
22
|
UserRole,
|
|
23
23
|
} from '@donotdev/core/server';
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
createEntityAwareMapper,
|
|
27
|
+
defaultFieldMapper,
|
|
28
|
+
getEntityFieldNames,
|
|
29
|
+
} from '@donotdev/supabase';
|
|
26
30
|
|
|
27
31
|
import { DoNotDevError } from '../../shared/utils.js';
|
|
28
32
|
import { createSupabaseHandler } from '../baseFunction.js';
|
|
@@ -32,7 +36,9 @@ const mapper = defaultFieldMapper;
|
|
|
32
36
|
|
|
33
37
|
/** Ensure we only pass strings to the mapper (entity listFields/ownership can be mis-typed at runtime). */
|
|
34
38
|
function toBackendColumn(field: unknown): string {
|
|
35
|
-
return mapper.toBackendField(
|
|
39
|
+
return mapper.toBackendField(
|
|
40
|
+
typeof field === 'string' ? field : String(field)
|
|
41
|
+
);
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
export interface ListEntityRequest {
|
|
@@ -58,7 +64,10 @@ function encodeCursor(id: string, orderBy: Record<string, any>): string {
|
|
|
58
64
|
/**
|
|
59
65
|
* Decode cursor for keyset pagination
|
|
60
66
|
*/
|
|
61
|
-
function decodeCursor(cursor: string): {
|
|
67
|
+
function decodeCursor(cursor: string): {
|
|
68
|
+
id: string;
|
|
69
|
+
orderBy: Record<string, any>;
|
|
70
|
+
} {
|
|
62
71
|
try {
|
|
63
72
|
return JSON.parse(Buffer.from(cursor, 'base64').toString());
|
|
64
73
|
} catch (error) {
|
|
@@ -132,7 +141,12 @@ export function createSupabaseListEntities(
|
|
|
132
141
|
const requestSchema = v.object({
|
|
133
142
|
where: v.optional(v.array(v.tuple([v.string(), v.any(), v.any()]))),
|
|
134
143
|
orderBy: v.optional(
|
|
135
|
-
v.array(
|
|
144
|
+
v.array(
|
|
145
|
+
v.tuple([
|
|
146
|
+
v.pipe(v.string(), v.minLength(1)),
|
|
147
|
+
v.picklist(['asc', 'desc']),
|
|
148
|
+
])
|
|
149
|
+
)
|
|
136
150
|
),
|
|
137
151
|
limit: v.optional(v.pipe(v.number(), v.minValue(1))),
|
|
138
152
|
startAfterId: v.optional(v.string()), // Offset-based (legacy)
|
|
@@ -149,18 +163,31 @@ export function createSupabaseListEntities(
|
|
|
149
163
|
isListCard ? `listCard_${collection}` : `list_${collection}`,
|
|
150
164
|
requestSchema,
|
|
151
165
|
async (data: ListEntityRequest, ctx) => {
|
|
152
|
-
const {
|
|
166
|
+
const {
|
|
167
|
+
where = [],
|
|
168
|
+
orderBy = [],
|
|
169
|
+
limit = 50,
|
|
170
|
+
startAfterId,
|
|
171
|
+
startAfterCursor,
|
|
172
|
+
search,
|
|
173
|
+
} = data;
|
|
153
174
|
const { userRole, uid, supabaseAdmin } = ctx;
|
|
154
175
|
|
|
155
176
|
const isAdmin = hasRoleAccess(userRole, 'admin');
|
|
156
177
|
|
|
157
178
|
// Build query - select fields (listFields + id + status, or *). Only use string entries (entity may be mis-typed).
|
|
158
|
-
const safeListFields = listFields?.filter(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
179
|
+
const safeListFields = listFields?.filter(
|
|
180
|
+
(f): f is string => typeof f === 'string'
|
|
181
|
+
);
|
|
182
|
+
const selectFields =
|
|
183
|
+
safeListFields && safeListFields.length > 0
|
|
184
|
+
? safeListFields.map((f) => toBackendColumn(f)).join(', ') +
|
|
185
|
+
', id, status'
|
|
186
|
+
: '*';
|
|
187
|
+
|
|
188
|
+
let query = supabaseAdmin
|
|
189
|
+
.from(collection)
|
|
190
|
+
.select(selectFields, { count: 'exact' });
|
|
164
191
|
|
|
165
192
|
// Filter out hidden statuses for non-admin users
|
|
166
193
|
if (!isAdmin) {
|
|
@@ -198,14 +225,20 @@ export function createSupabaseListEntities(
|
|
|
198
225
|
);
|
|
199
226
|
}
|
|
200
227
|
// Escape SQL ILIKE wildcards to prevent wildcard injection
|
|
201
|
-
const escapedQuery = search.query
|
|
228
|
+
const escapedQuery = search.query
|
|
229
|
+
.replace(/%/g, '\\%')
|
|
230
|
+
.replace(/_/g, '\\_');
|
|
202
231
|
query = query.ilike(toBackendColumn(search.field), `%${escapedQuery}%`);
|
|
203
232
|
}
|
|
204
233
|
|
|
205
234
|
// Validate where clause fields against entity schema
|
|
206
235
|
for (const [field, operator, value] of where) {
|
|
207
236
|
if (safeListFields && safeListFields.length > 0) {
|
|
208
|
-
if (
|
|
237
|
+
if (
|
|
238
|
+
!safeListFields.includes(field) &&
|
|
239
|
+
field !== 'status' &&
|
|
240
|
+
field !== 'id'
|
|
241
|
+
) {
|
|
209
242
|
throw new DoNotDevError(
|
|
210
243
|
`Where field '${field}' is not allowed`,
|
|
211
244
|
'invalid-argument'
|
|
@@ -220,9 +253,13 @@ export function createSupabaseListEntities(
|
|
|
220
253
|
query = applyOperator(query, toBackendColumn(field), operator, value);
|
|
221
254
|
}
|
|
222
255
|
|
|
223
|
-
const hasIdInOrderBy = orderBy.some(
|
|
256
|
+
const hasIdInOrderBy = orderBy.some(
|
|
257
|
+
([field]) => toBackendColumn(field) === 'id'
|
|
258
|
+
);
|
|
224
259
|
for (const [field, direction] of orderBy) {
|
|
225
|
-
query = query.order(toBackendColumn(field), {
|
|
260
|
+
query = query.order(toBackendColumn(field), {
|
|
261
|
+
ascending: direction === 'asc',
|
|
262
|
+
});
|
|
226
263
|
}
|
|
227
264
|
// Add id as tiebreaker if not already in orderBy
|
|
228
265
|
if (!hasIdInOrderBy && orderBy.length > 0) {
|
|
@@ -236,7 +273,7 @@ export function createSupabaseListEntities(
|
|
|
236
273
|
useKeysetPagination = true;
|
|
237
274
|
try {
|
|
238
275
|
const cursor = decodeCursor(startAfterCursor);
|
|
239
|
-
|
|
276
|
+
|
|
240
277
|
if (orderBy.length === 0) {
|
|
241
278
|
query = query.gt('id', cursor.id);
|
|
242
279
|
} else {
|
|
@@ -271,12 +308,17 @@ export function createSupabaseListEntities(
|
|
|
271
308
|
const { data: rows, error, count } = await query;
|
|
272
309
|
|
|
273
310
|
if (error) {
|
|
274
|
-
throw new DoNotDevError(
|
|
311
|
+
throw new DoNotDevError(
|
|
312
|
+
`Failed to list entities: ${error.message}`,
|
|
313
|
+
'internal'
|
|
314
|
+
);
|
|
275
315
|
}
|
|
276
316
|
|
|
277
317
|
let items = (rows || []) as Record<string, any>[];
|
|
278
|
-
|
|
279
|
-
// Filter out cursor item for keyset pagination
|
|
318
|
+
|
|
319
|
+
// Filter out cursor item for keyset pagination.
|
|
320
|
+
// Note: items are still raw DB rows here (pre-normalization), so mapper.toBackendField
|
|
321
|
+
// is correct for accessing backend column values. entityMapper is used later for normalization.
|
|
280
322
|
if (useKeysetPagination && startAfterCursor && items.length > 0) {
|
|
281
323
|
try {
|
|
282
324
|
const cursor = decodeCursor(startAfterCursor);
|
|
@@ -320,11 +362,23 @@ export function createSupabaseListEntities(
|
|
|
320
362
|
return value;
|
|
321
363
|
};
|
|
322
364
|
|
|
323
|
-
const visibilityOptions =
|
|
365
|
+
const visibilityOptions =
|
|
366
|
+
ownership && uid ? { uid, ownership } : undefined;
|
|
367
|
+
|
|
368
|
+
// Build entity-aware mapper from schema entries so fromBackendRow returns
|
|
369
|
+
// entity field names (not blind camelCase), matching filterVisibleFields expectations.
|
|
370
|
+
const entityFieldNames = getEntityFieldNames(documentSchema);
|
|
371
|
+
const entityMapper =
|
|
372
|
+
entityFieldNames.length > 0
|
|
373
|
+
? createEntityAwareMapper(entityFieldNames)
|
|
374
|
+
: mapper;
|
|
324
375
|
|
|
325
376
|
// Filter document fields based on visibility and user role
|
|
326
377
|
const filteredItems = items.map((row) => {
|
|
327
|
-
const camelRow =
|
|
378
|
+
const camelRow = entityMapper.fromBackendRow(row) as Record<
|
|
379
|
+
string,
|
|
380
|
+
any
|
|
381
|
+
>;
|
|
328
382
|
const visibleData = filterVisibleFields(
|
|
329
383
|
camelRow,
|
|
330
384
|
documentSchema,
|
|
@@ -355,7 +409,7 @@ export function createSupabaseListEntities(
|
|
|
355
409
|
});
|
|
356
410
|
|
|
357
411
|
const hasMore = items.length === limit;
|
|
358
|
-
|
|
412
|
+
|
|
359
413
|
// Generate cursor for keyset pagination or offset for legacy
|
|
360
414
|
let lastVisible: string | null = null;
|
|
361
415
|
if (hasMore && filteredItems.length > 0) {
|
|
@@ -18,10 +18,7 @@ import { defaultFieldMapper } from '@donotdev/supabase';
|
|
|
18
18
|
import { updateMetadata } from '../../shared/index.js';
|
|
19
19
|
import { DoNotDevError, validateDocument } from '../../shared/utils.js';
|
|
20
20
|
import { createSupabaseHandler } from '../baseFunction.js';
|
|
21
|
-
import {
|
|
22
|
-
checkIdempotency,
|
|
23
|
-
storeIdempotency,
|
|
24
|
-
} from '../utils/idempotency.js';
|
|
21
|
+
import { checkIdempotency, storeIdempotency } from '../utils/idempotency.js';
|
|
25
22
|
|
|
26
23
|
const mapper = defaultFieldMapper;
|
|
27
24
|
|
|
@@ -68,7 +65,10 @@ async function checkUniqueKeys(
|
|
|
68
65
|
// Build query excluding current document
|
|
69
66
|
let query = supabaseAdmin.from(collection).select('*');
|
|
70
67
|
for (const field of uniqueKey.fields) {
|
|
71
|
-
query = query.eq(
|
|
68
|
+
query = query.eq(
|
|
69
|
+
mapper.toBackendField(field),
|
|
70
|
+
normalizeValue(payload[field])
|
|
71
|
+
);
|
|
72
72
|
}
|
|
73
73
|
query = query.neq('id', id);
|
|
74
74
|
|
|
@@ -139,7 +139,10 @@ export function createSupabaseUpdateEntity(
|
|
|
139
139
|
throw new DoNotDevError('Entity not found', 'not-found');
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
const merged = {
|
|
142
|
+
const merged = {
|
|
143
|
+
...(mapper.fromBackendRow(existing) as Record<string, any>),
|
|
144
|
+
...payload,
|
|
145
|
+
};
|
|
143
146
|
const status = merged.status ?? existing.status;
|
|
144
147
|
const isDraft = status === 'draft';
|
|
145
148
|
|
|
@@ -150,7 +153,14 @@ export function createSupabaseUpdateEntity(
|
|
|
150
153
|
const uniqueKeys = schemaWithMeta.metadata?.uniqueKeys;
|
|
151
154
|
|
|
152
155
|
if (uniqueKeys && uniqueKeys.length > 0) {
|
|
153
|
-
await checkUniqueKeys(
|
|
156
|
+
await checkUniqueKeys(
|
|
157
|
+
collection,
|
|
158
|
+
id,
|
|
159
|
+
merged,
|
|
160
|
+
uniqueKeys,
|
|
161
|
+
isDraft,
|
|
162
|
+
supabaseAdmin
|
|
163
|
+
);
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
// Validate merged document (skip for drafts)
|
|
@@ -159,9 +169,18 @@ export function createSupabaseUpdateEntity(
|
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
const metadata = updateMetadata(uid);
|
|
162
|
-
const snakeMetadata = mapper.toBackendKeys(
|
|
172
|
+
const snakeMetadata = mapper.toBackendKeys(
|
|
173
|
+
metadata as Record<string, unknown>
|
|
174
|
+
);
|
|
163
175
|
|
|
164
|
-
const {
|
|
176
|
+
const {
|
|
177
|
+
createdAt,
|
|
178
|
+
updatedAt,
|
|
179
|
+
created_at,
|
|
180
|
+
updated_at,
|
|
181
|
+
id: _id,
|
|
182
|
+
...payloadWithoutTimestamps
|
|
183
|
+
} = payload;
|
|
165
184
|
const snakePayload = mapper.toBackendKeys(payloadWithoutTimestamps);
|
|
166
185
|
|
|
167
186
|
// Update document (DB sets updated_at via trigger)
|
|
@@ -176,7 +195,10 @@ export function createSupabaseUpdateEntity(
|
|
|
176
195
|
.single();
|
|
177
196
|
|
|
178
197
|
if (error) {
|
|
179
|
-
throw new DoNotDevError(
|
|
198
|
+
throw new DoNotDevError(
|
|
199
|
+
`Failed to update entity: ${error.message}`,
|
|
200
|
+
'internal'
|
|
201
|
+
);
|
|
180
202
|
}
|
|
181
203
|
|
|
182
204
|
const result = mapper.fromBackendRow(updated) as Record<string, any>;
|
|
@@ -28,10 +28,13 @@ import type { SupabaseClient } from '@supabase/supabase-js';
|
|
|
28
28
|
* @version 0.0.1
|
|
29
29
|
* @since 0.5.0
|
|
30
30
|
*/
|
|
31
|
-
export function createSupabaseAuthProvider(
|
|
31
|
+
export function createSupabaseAuthProvider(
|
|
32
|
+
supabaseAdmin: SupabaseClient
|
|
33
|
+
): AuthProvider {
|
|
32
34
|
return {
|
|
33
35
|
async getUser(userId: string) {
|
|
34
|
-
const { data, error } =
|
|
36
|
+
const { data, error } =
|
|
37
|
+
await supabaseAdmin.auth.admin.getUserById(userId);
|
|
35
38
|
if (error) throw error;
|
|
36
39
|
return { customClaims: data.user?.app_metadata ?? {} };
|
|
37
40
|
},
|
package/src/supabase/index.ts
CHANGED
|
@@ -60,10 +60,7 @@ export {
|
|
|
60
60
|
checkRateLimitWithPostgres,
|
|
61
61
|
DEFAULT_RATE_LIMITS,
|
|
62
62
|
} from './utils/rateLimiter.js';
|
|
63
|
-
export type {
|
|
64
|
-
RateLimitConfig,
|
|
65
|
-
RateLimitResult,
|
|
66
|
-
} from './utils/rateLimiter.js';
|
|
63
|
+
export type { RateLimitConfig, RateLimitResult } from './utils/rateLimiter.js';
|
|
67
64
|
export {
|
|
68
65
|
recordOperationMetrics,
|
|
69
66
|
getFailureRate,
|
|
@@ -89,7 +89,7 @@ export function createSupabaseCrudFunctions(
|
|
|
89
89
|
schemas.update,
|
|
90
90
|
access.update
|
|
91
91
|
);
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
// Extract reference metadata from entity if available
|
|
94
94
|
const schemaWithMeta = schemas.get as {
|
|
95
95
|
metadata?: {
|
|
@@ -108,7 +108,7 @@ export function createSupabaseCrudFunctions(
|
|
|
108
108
|
};
|
|
109
109
|
};
|
|
110
110
|
const referenceMetadata = schemaWithMeta.metadata?.references;
|
|
111
|
-
|
|
111
|
+
|
|
112
112
|
handlers[`delete_${col}`] = createSupabaseDeleteEntity(
|
|
113
113
|
col,
|
|
114
114
|
access.delete,
|
|
@@ -127,7 +127,8 @@ export function createSupabaseCrudFunctions(
|
|
|
127
127
|
const serve = async (req: Request): Promise<Response> => {
|
|
128
128
|
try {
|
|
129
129
|
const body = await req.json().catch(() => ({}));
|
|
130
|
-
const functionName = (body as Record<string, unknown>)
|
|
130
|
+
const functionName = (body as Record<string, unknown>)
|
|
131
|
+
._functionName as string;
|
|
131
132
|
|
|
132
133
|
if (!functionName) {
|
|
133
134
|
return new Response(
|
|
@@ -146,7 +147,7 @@ export function createSupabaseCrudFunctions(
|
|
|
146
147
|
|
|
147
148
|
// Remove _functionName from body before passing to handler
|
|
148
149
|
const { _functionName, ...handlerData } = body as Record<string, unknown>;
|
|
149
|
-
|
|
150
|
+
|
|
150
151
|
// Create new request with cleaned body
|
|
151
152
|
const handlerReq = new Request(req.url, {
|
|
152
153
|
method: req.method,
|
|
@@ -156,11 +157,12 @@ export function createSupabaseCrudFunctions(
|
|
|
156
157
|
|
|
157
158
|
return handler(handlerReq);
|
|
158
159
|
} catch (error) {
|
|
159
|
-
const message =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
const message =
|
|
161
|
+
error instanceof Error ? error.message : 'Internal server error';
|
|
162
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
163
|
+
status: 500,
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
});
|
|
164
166
|
}
|
|
165
167
|
};
|
|
166
168
|
|
|
@@ -86,21 +86,19 @@ export async function storeIdempotency<T>(
|
|
|
86
86
|
const expiresAt = new Date();
|
|
87
87
|
expiresAt.setHours(expiresAt.getHours() + ttl);
|
|
88
88
|
|
|
89
|
-
const { error } = await supabaseAdmin
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
);
|
|
89
|
+
const { error } = await supabaseAdmin.from('idempotency').upsert(
|
|
90
|
+
{
|
|
91
|
+
id,
|
|
92
|
+
operation,
|
|
93
|
+
idempotency_key: idempotencyKey,
|
|
94
|
+
result: result as any,
|
|
95
|
+
processed_by: uid,
|
|
96
|
+
expires_at: expiresAt.toISOString(),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
onConflict: 'idempotency_key',
|
|
100
|
+
}
|
|
101
|
+
);
|
|
104
102
|
|
|
105
103
|
if (error) {
|
|
106
104
|
console.error('[idempotency] Store failed:', error);
|
|
@@ -167,7 +167,11 @@ export async function getSlowOperations(
|
|
|
167
167
|
grouped[metric.operation].count += 1;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
const results: Array<{
|
|
170
|
+
const results: Array<{
|
|
171
|
+
operation: string;
|
|
172
|
+
avgDuration: number;
|
|
173
|
+
count: number;
|
|
174
|
+
}> = [];
|
|
171
175
|
for (const [operation, stats] of Object.entries(grouped)) {
|
|
172
176
|
const avgDuration = stats.sum / stats.count;
|
|
173
177
|
if (avgDuration >= thresholdMs) {
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
27
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
ServerRateLimitConfig as RateLimitConfig,
|
|
29
|
+
ServerRateLimitResult as RateLimitResult,
|
|
30
|
+
} from '@donotdev/core';
|
|
28
31
|
|
|
29
32
|
export type { RateLimitConfig, RateLimitResult };
|
|
30
33
|
|
|
@@ -198,7 +201,11 @@ export async function checkRateLimitWithPostgres(
|
|
|
198
201
|
|
|
199
202
|
if (error) {
|
|
200
203
|
// Log for observability — infrastructure errors here need immediate attention.
|
|
201
|
-
console.error(
|
|
204
|
+
console.error(
|
|
205
|
+
'[rateLimit] rate_limit_check RPC failed:',
|
|
206
|
+
error.message,
|
|
207
|
+
error.code
|
|
208
|
+
);
|
|
202
209
|
return failClosed();
|
|
203
210
|
}
|
|
204
211
|
|
|
@@ -210,7 +217,10 @@ export async function checkRateLimitWithPostgres(
|
|
|
210
217
|
blockRemainingSeconds: result.block_remaining_seconds,
|
|
211
218
|
};
|
|
212
219
|
} catch (error) {
|
|
213
|
-
console.error(
|
|
220
|
+
console.error(
|
|
221
|
+
'[rateLimit] Unexpected error in checkRateLimitWithPostgres:',
|
|
222
|
+
error
|
|
223
|
+
);
|
|
214
224
|
return failClosed();
|
|
215
225
|
}
|
|
216
226
|
}
|
|
@@ -55,7 +55,10 @@ export function createStripeWebhook(billingConfig: StripeBackConfig) {
|
|
|
55
55
|
async getUser(userId: string) {
|
|
56
56
|
return getFirebaseAdminAuth().getUser(userId);
|
|
57
57
|
},
|
|
58
|
-
async setCustomUserClaims(
|
|
58
|
+
async setCustomUserClaims(
|
|
59
|
+
userId: string,
|
|
60
|
+
claims: Record<string, unknown>
|
|
61
|
+
) {
|
|
59
62
|
await getFirebaseAdminAuth().setCustomUserClaims(userId, claims);
|
|
60
63
|
},
|
|
61
64
|
};
|
|
@@ -90,7 +93,8 @@ async function getRawBody(req: NextApiRequest): Promise<Buffer> {
|
|
|
90
93
|
const chunks: Buffer[] = [];
|
|
91
94
|
let totalBytes = 0;
|
|
92
95
|
for await (const chunk of req) {
|
|
93
|
-
const buf =
|
|
96
|
+
const buf =
|
|
97
|
+
typeof chunk === 'string' ? Buffer.from(chunk) : (chunk as Buffer);
|
|
94
98
|
totalBytes += buf.length;
|
|
95
99
|
if (totalBytes > MAX_BODY_BYTES) {
|
|
96
100
|
throw new Error('Request body too large');
|
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
transformFirestoreData,
|
|
20
20
|
} from '../../../shared/index.js';
|
|
21
21
|
import { createMetadata } from '../../../shared/index.js';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
validateCollectionName,
|
|
24
|
+
validateDocument,
|
|
25
|
+
} from '../../../shared/utils.js';
|
|
23
26
|
import { verifyAuthToken } from '../../../shared/utils/internal/auth.js';
|
|
24
27
|
|
|
25
28
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
@@ -84,7 +87,9 @@ export default async function handler(
|
|
|
84
87
|
handleError(error);
|
|
85
88
|
} catch (handledError: any) {
|
|
86
89
|
const status = handledError.code === 'invalid-argument' ? 400 : 500;
|
|
87
|
-
return res
|
|
90
|
+
return res
|
|
91
|
+
.status(status)
|
|
92
|
+
.json({ error: handledError.message, code: handledError.code });
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
}
|
|
@@ -60,7 +60,9 @@ export default async function handler(
|
|
|
60
60
|
handleError(error);
|
|
61
61
|
} catch (handledError: any) {
|
|
62
62
|
const status = handledError.code === 'invalid-argument' ? 400 : 500;
|
|
63
|
-
return res
|
|
63
|
+
return res
|
|
64
|
+
.status(status)
|
|
65
|
+
.json({ error: handledError.message, code: handledError.code });
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
}
|
|
@@ -74,7 +74,9 @@ export default async function handler(
|
|
|
74
74
|
handleError(error);
|
|
75
75
|
} catch (handledError: any) {
|
|
76
76
|
const status = handledError.code === 'invalid-argument' ? 400 : 500;
|
|
77
|
-
return res
|
|
77
|
+
return res
|
|
78
|
+
.status(status)
|
|
79
|
+
.json({ error: handledError.message, code: handledError.code });
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
}
|
|
@@ -87,7 +87,9 @@ export default async function handler(
|
|
|
87
87
|
handleError(error);
|
|
88
88
|
} catch (handledError: any) {
|
|
89
89
|
const status = handledError.code === 'invalid-argument' ? 400 : 500;
|
|
90
|
-
return res
|
|
90
|
+
return res
|
|
91
|
+
.status(status)
|
|
92
|
+
.json({ error: handledError.message, code: handledError.code });
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
}
|
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
transformFirestoreData,
|
|
20
20
|
} from '../../../shared/index.js';
|
|
21
21
|
import { updateMetadata } from '../../../shared/index.js';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
validateCollectionName,
|
|
24
|
+
validateDocument,
|
|
25
|
+
} from '../../../shared/utils.js';
|
|
23
26
|
import { verifyAuthToken } from '../../../shared/utils/internal/auth.js';
|
|
24
27
|
|
|
25
28
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
@@ -100,7 +103,9 @@ export default async function handler(
|
|
|
100
103
|
handleError(error);
|
|
101
104
|
} catch (handledError: any) {
|
|
102
105
|
const status = handledError.code === 'invalid-argument' ? 400 : 500;
|
|
103
|
-
return res
|
|
106
|
+
return res
|
|
107
|
+
.status(status)
|
|
108
|
+
.json({ error: handledError.message, code: handledError.code });
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
}
|