@autumnsgrove/groveengine 0.4.12 → 0.6.1

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 (48) hide show
  1. package/README.md +5 -3
  2. package/dist/components/OnboardingChecklist.svelte +118 -0
  3. package/dist/components/OnboardingChecklist.svelte.d.ts +14 -0
  4. package/dist/components/quota/QuotaWarning.svelte +125 -0
  5. package/dist/components/quota/QuotaWarning.svelte.d.ts +16 -0
  6. package/dist/components/quota/QuotaWidget.svelte +120 -0
  7. package/dist/components/quota/QuotaWidget.svelte.d.ts +15 -0
  8. package/dist/components/quota/UpgradePrompt.svelte +288 -0
  9. package/dist/components/quota/UpgradePrompt.svelte.d.ts +13 -0
  10. package/dist/components/quota/index.d.ts +8 -0
  11. package/dist/components/quota/index.js +8 -0
  12. package/dist/groveauth/client.d.ts +143 -0
  13. package/dist/groveauth/client.js +502 -0
  14. package/dist/groveauth/colors.d.ts +35 -0
  15. package/dist/groveauth/colors.js +91 -0
  16. package/dist/groveauth/index.d.ts +34 -0
  17. package/dist/groveauth/index.js +35 -0
  18. package/dist/groveauth/limits.d.ts +70 -0
  19. package/dist/groveauth/limits.js +202 -0
  20. package/dist/groveauth/rate-limit.d.ts +95 -0
  21. package/dist/groveauth/rate-limit.js +172 -0
  22. package/dist/groveauth/types.d.ts +139 -0
  23. package/dist/groveauth/types.js +61 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +4 -0
  26. package/dist/payments/types.d.ts +7 -2
  27. package/dist/server/services/__mocks__/cloudflare.d.ts +54 -0
  28. package/dist/server/services/__mocks__/cloudflare.js +470 -0
  29. package/dist/server/services/cache.d.ts +170 -0
  30. package/dist/server/services/cache.js +335 -0
  31. package/dist/server/services/database.d.ts +236 -0
  32. package/dist/server/services/database.js +450 -0
  33. package/dist/server/services/index.d.ts +34 -0
  34. package/dist/server/services/index.js +77 -0
  35. package/dist/server/services/storage.d.ts +221 -0
  36. package/dist/server/services/storage.js +485 -0
  37. package/package.json +11 -1
  38. package/static/fonts/Calistoga-Regular.ttf +1438 -0
  39. package/static/fonts/Caveat-Regular.ttf +0 -0
  40. package/static/fonts/EBGaramond-Regular.ttf +0 -0
  41. package/static/fonts/Fraunces-Regular.ttf +0 -0
  42. package/static/fonts/InstrumentSans-Regular.ttf +0 -0
  43. package/static/fonts/Lora-Regular.ttf +0 -0
  44. package/static/fonts/Luciole-Regular.ttf +1438 -0
  45. package/static/fonts/Manrope-Regular.ttf +0 -0
  46. package/static/fonts/Merriweather-Regular.ttf +1439 -0
  47. package/static/fonts/Nunito-Regular.ttf +0 -0
  48. package/static/fonts/PlusJakartaSans-Regular.ttf +0 -0
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Mock implementations for Cloudflare services (D1, KV, R2)
3
+ * Used for testing the service abstraction layer
4
+ */
5
+ import { vi } from 'vitest';
6
+ /**
7
+ * Creates a mock D1 database with in-memory storage
8
+ */
9
+ export function createMockD1() {
10
+ const tables = new Map();
11
+ const createStatement = (sql) => {
12
+ let boundParams = [];
13
+ const statement = {
14
+ bind: (...params) => {
15
+ boundParams = params;
16
+ return statement;
17
+ },
18
+ first: vi.fn(async () => {
19
+ const result = await statement.all();
20
+ return result.results[0] ?? null;
21
+ }),
22
+ all: vi.fn(async () => {
23
+ const result = executeSql(sql, boundParams, tables);
24
+ return {
25
+ results: result.results,
26
+ success: result.success,
27
+ meta: result.meta
28
+ };
29
+ }),
30
+ run: vi.fn(async () => {
31
+ return executeSql(sql, boundParams, tables);
32
+ }),
33
+ raw: vi.fn(async () => {
34
+ const result = await statement.all();
35
+ return result.results.map((row) => Object.values(row));
36
+ })
37
+ };
38
+ return statement;
39
+ };
40
+ const db = {
41
+ _tables: tables,
42
+ prepare: vi.fn((sql) => createStatement(sql)),
43
+ batch: vi.fn(async (statements) => {
44
+ const results = [];
45
+ for (const stmt of statements) {
46
+ results.push(await stmt.run());
47
+ }
48
+ return results;
49
+ }),
50
+ exec: vi.fn(async (sql) => {
51
+ // Simple exec - runs raw SQL
52
+ return { count: 1, duration: 0 };
53
+ }),
54
+ dump: vi.fn(async () => new ArrayBuffer(0)),
55
+ withSession: vi.fn(() => createMockD1Session(tables))
56
+ };
57
+ return db;
58
+ }
59
+ /**
60
+ * Creates a mock D1 session (for read replication testing)
61
+ */
62
+ function createMockD1Session(tables) {
63
+ const createStatement = (sql) => {
64
+ let boundParams = [];
65
+ const statement = {
66
+ bind: (...params) => {
67
+ boundParams = params;
68
+ return statement;
69
+ },
70
+ first: vi.fn(async () => {
71
+ const result = await statement.all();
72
+ return result.results[0] ?? null;
73
+ }),
74
+ all: vi.fn(async () => {
75
+ const result = executeSql(sql, boundParams, tables);
76
+ return {
77
+ results: result.results,
78
+ success: result.success,
79
+ meta: result.meta
80
+ };
81
+ }),
82
+ run: vi.fn(async () => {
83
+ return executeSql(sql, boundParams, tables);
84
+ }),
85
+ raw: vi.fn(async () => {
86
+ const result = await statement.all();
87
+ return result.results.map((row) => Object.values(row));
88
+ })
89
+ };
90
+ return statement;
91
+ };
92
+ return {
93
+ prepare: vi.fn((sql) => createStatement(sql))
94
+ };
95
+ }
96
+ /**
97
+ * Simple SQL parser/executor for mocking
98
+ * Supports basic SELECT, INSERT, UPDATE, DELETE operations
99
+ */
100
+ function executeSql(sql, params, tables) {
101
+ const normalizedSql = sql.trim().toUpperCase();
102
+ let paramIndex = 0;
103
+ const getNextParam = () => params[paramIndex++];
104
+ // Extract table name from SQL
105
+ const tableMatch = sql.match(/(?:FROM|INTO|UPDATE)\s+(\w+)/i);
106
+ const tableName = tableMatch?.[1]?.toLowerCase() ?? 'unknown';
107
+ // Ensure table exists
108
+ if (!tables.has(tableName)) {
109
+ tables.set(tableName, []);
110
+ }
111
+ const tableData = tables.get(tableName);
112
+ if (normalizedSql.startsWith('SELECT')) {
113
+ // Handle SELECT queries
114
+ const whereMatch = sql.match(/WHERE\s+(.+?)(?:\s+ORDER|\s+LIMIT|$)/i);
115
+ let results = [...tableData];
116
+ if (whereMatch) {
117
+ results = filterRows(results, whereMatch[1], params);
118
+ }
119
+ // Handle LIMIT
120
+ const limitMatch = sql.match(/LIMIT\s+(\d+)/i);
121
+ if (limitMatch) {
122
+ results = results.slice(0, parseInt(limitMatch[1], 10));
123
+ }
124
+ return {
125
+ results,
126
+ success: true,
127
+ meta: {
128
+ changes: 0,
129
+ duration: 1,
130
+ last_row_id: 0,
131
+ rows_read: results.length,
132
+ rows_written: 0
133
+ }
134
+ };
135
+ }
136
+ if (normalizedSql.startsWith('INSERT')) {
137
+ // Handle INSERT queries
138
+ const columnsMatch = sql.match(/\(([^)]+)\)\s+VALUES/i);
139
+ const columns = columnsMatch?.[1]?.split(',').map((c) => c.trim()) ?? [];
140
+ const row = {};
141
+ for (const col of columns) {
142
+ row[col] = getNextParam();
143
+ }
144
+ tableData.push(row);
145
+ return {
146
+ results: [],
147
+ success: true,
148
+ meta: {
149
+ changes: 1,
150
+ duration: 1,
151
+ last_row_id: tableData.length,
152
+ rows_read: 0,
153
+ rows_written: 1
154
+ }
155
+ };
156
+ }
157
+ if (normalizedSql.startsWith('UPDATE')) {
158
+ // Handle UPDATE queries
159
+ const setMatch = sql.match(/SET\s+(.+?)\s+WHERE/i);
160
+ const whereMatch = sql.match(/WHERE\s+(.+)$/i);
161
+ if (!setMatch || !whereMatch) {
162
+ return {
163
+ results: [],
164
+ success: false,
165
+ meta: { changes: 0, duration: 1, last_row_id: 0, rows_read: 0, rows_written: 0 }
166
+ };
167
+ }
168
+ // Parse SET clause
169
+ const setClauses = setMatch[1].split(',').map((s) => s.trim());
170
+ const updates = {};
171
+ for (const clause of setClauses) {
172
+ const [col] = clause.split('=').map((s) => s.trim());
173
+ updates[col] = getNextParam();
174
+ }
175
+ // Find and update matching rows
176
+ let changes = 0;
177
+ const whereValue = getNextParam();
178
+ for (const row of tableData) {
179
+ // Simple WHERE id = ? matching
180
+ if (whereMatch[1].includes('id = ?') && row.id === whereValue) {
181
+ Object.assign(row, updates);
182
+ changes++;
183
+ }
184
+ }
185
+ return {
186
+ results: [],
187
+ success: true,
188
+ meta: {
189
+ changes,
190
+ duration: 1,
191
+ last_row_id: 0,
192
+ rows_read: changes,
193
+ rows_written: changes
194
+ }
195
+ };
196
+ }
197
+ if (normalizedSql.startsWith('DELETE')) {
198
+ // Handle DELETE queries
199
+ const whereMatch = sql.match(/WHERE\s+(.+)$/i);
200
+ if (!whereMatch) {
201
+ const changes = tableData.length;
202
+ tables.set(tableName, []);
203
+ return {
204
+ results: [],
205
+ success: true,
206
+ meta: { changes, duration: 1, last_row_id: 0, rows_read: 0, rows_written: changes }
207
+ };
208
+ }
209
+ const initialLength = tableData.length;
210
+ const filtered = filterRows(tableData, whereMatch[1], params, true);
211
+ tables.set(tableName, filtered);
212
+ const changes = initialLength - filtered.length;
213
+ return {
214
+ results: [],
215
+ success: true,
216
+ meta: {
217
+ changes,
218
+ duration: 1,
219
+ last_row_id: 0,
220
+ rows_read: 0,
221
+ rows_written: changes
222
+ }
223
+ };
224
+ }
225
+ // Default fallback
226
+ return {
227
+ results: [],
228
+ success: true,
229
+ meta: { changes: 0, duration: 1, last_row_id: 0, rows_read: 0, rows_written: 0 }
230
+ };
231
+ }
232
+ /**
233
+ * Simple row filtering for WHERE clauses
234
+ */
235
+ function filterRows(rows, whereClause, params, invert = false) {
236
+ // Very simple WHERE parsing - supports "column = ?" patterns
237
+ const conditions = whereClause.split(/\s+AND\s+/i);
238
+ let paramIndex = 0;
239
+ return rows.filter((row) => {
240
+ let matches = true;
241
+ for (const condition of conditions) {
242
+ const match = condition.match(/(\w+)\s*=\s*\?/);
243
+ if (match) {
244
+ const col = match[1];
245
+ const value = params[paramIndex++];
246
+ if (row[col] !== value) {
247
+ matches = false;
248
+ break;
249
+ }
250
+ }
251
+ }
252
+ return invert ? !matches : matches;
253
+ });
254
+ }
255
+ /**
256
+ * Creates a mock KV namespace with in-memory storage
257
+ */
258
+ export function createMockKV() {
259
+ const store = new Map();
260
+ const kv = {
261
+ _store: store,
262
+ get: vi.fn(async (key, options) => {
263
+ const entry = store.get(key);
264
+ if (!entry)
265
+ return null;
266
+ // Check expiration
267
+ if (entry.expiresAt && entry.expiresAt < Date.now()) {
268
+ store.delete(key);
269
+ return null;
270
+ }
271
+ return entry.value;
272
+ }),
273
+ getWithMetadata: vi.fn(async (key) => {
274
+ const entry = store.get(key);
275
+ if (!entry)
276
+ return { value: null, metadata: null };
277
+ if (entry.expiresAt && entry.expiresAt < Date.now()) {
278
+ store.delete(key);
279
+ return { value: null, metadata: null };
280
+ }
281
+ return { value: entry.value, metadata: entry.metadata ?? null };
282
+ }),
283
+ put: vi.fn(async (key, value, options) => {
284
+ const entry = { value };
285
+ if (options?.expirationTtl) {
286
+ entry.expiresAt = Date.now() + options.expirationTtl * 1000;
287
+ }
288
+ if (options?.metadata) {
289
+ entry.metadata = options.metadata;
290
+ }
291
+ store.set(key, entry);
292
+ }),
293
+ delete: vi.fn(async (key) => {
294
+ store.delete(key);
295
+ }),
296
+ list: vi.fn(async (options) => {
297
+ const keys = [];
298
+ const prefix = options?.prefix ?? '';
299
+ const limit = options?.limit ?? 1000;
300
+ for (const [key, entry] of store.entries()) {
301
+ if (key.startsWith(prefix)) {
302
+ // Skip expired entries
303
+ if (entry.expiresAt && entry.expiresAt < Date.now()) {
304
+ continue;
305
+ }
306
+ keys.push({
307
+ name: key,
308
+ expiration: entry.expiresAt ? Math.floor(entry.expiresAt / 1000) : undefined,
309
+ metadata: entry.metadata
310
+ });
311
+ if (keys.length >= limit)
312
+ break;
313
+ }
314
+ }
315
+ return {
316
+ keys,
317
+ list_complete: keys.length < limit,
318
+ cursor: keys.length >= limit ? 'next-cursor' : undefined
319
+ };
320
+ })
321
+ };
322
+ return kv;
323
+ }
324
+ /**
325
+ * Creates a mock R2 bucket with in-memory storage
326
+ */
327
+ export function createMockR2() {
328
+ const objects = new Map();
329
+ const createR2Object = (key, entry) => ({
330
+ key,
331
+ version: 'v1',
332
+ size: entry.size,
333
+ etag: entry.etag,
334
+ httpEtag: `"${entry.etag}"`,
335
+ uploaded: entry.uploaded,
336
+ httpMetadata: entry.httpMetadata,
337
+ customMetadata: entry.customMetadata,
338
+ range: undefined,
339
+ checksums: {
340
+ toJSON: () => ({})
341
+ },
342
+ storageClass: 'Standard',
343
+ writeHttpMetadata: vi.fn()
344
+ });
345
+ const createR2ObjectBody = (key, entry) => ({
346
+ ...createR2Object(key, entry),
347
+ body: new ReadableStream({
348
+ start(controller) {
349
+ controller.enqueue(new Uint8Array(entry.body));
350
+ controller.close();
351
+ }
352
+ }),
353
+ bodyUsed: false,
354
+ arrayBuffer: vi.fn(async () => entry.body),
355
+ text: vi.fn(async () => new TextDecoder().decode(entry.body)),
356
+ json: vi.fn(async () => JSON.parse(new TextDecoder().decode(entry.body))),
357
+ blob: vi.fn(async () => new Blob([entry.body]))
358
+ });
359
+ const bucket = {
360
+ _objects: objects,
361
+ head: vi.fn(async (key) => {
362
+ const entry = objects.get(key);
363
+ if (!entry)
364
+ return null;
365
+ return createR2Object(key, entry);
366
+ }),
367
+ get: vi.fn(async (key) => {
368
+ const entry = objects.get(key);
369
+ if (!entry)
370
+ return null;
371
+ return createR2ObjectBody(key, entry);
372
+ }),
373
+ put: vi.fn(async (key, value, options) => {
374
+ let body;
375
+ if (value instanceof ArrayBuffer) {
376
+ body = value;
377
+ }
378
+ else if (typeof value === 'string') {
379
+ body = new TextEncoder().encode(value).buffer;
380
+ }
381
+ else if (value instanceof Blob) {
382
+ body = await value.arrayBuffer();
383
+ }
384
+ else {
385
+ // ReadableStream
386
+ const reader = value.getReader();
387
+ const chunks = [];
388
+ let done = false;
389
+ while (!done) {
390
+ const { value: chunk, done: isDone } = await reader.read();
391
+ if (chunk)
392
+ chunks.push(chunk);
393
+ done = isDone;
394
+ }
395
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
396
+ const result = new Uint8Array(totalLength);
397
+ let offset = 0;
398
+ for (const chunk of chunks) {
399
+ result.set(chunk, offset);
400
+ offset += chunk.length;
401
+ }
402
+ body = result.buffer;
403
+ }
404
+ const etag = `etag-${Date.now()}-${Math.random().toString(36).slice(2)}`;
405
+ const entry = {
406
+ body,
407
+ httpMetadata: options?.httpMetadata,
408
+ customMetadata: options?.customMetadata,
409
+ etag,
410
+ uploaded: new Date(),
411
+ size: body.byteLength
412
+ };
413
+ objects.set(key, entry);
414
+ return createR2Object(key, entry);
415
+ }),
416
+ delete: vi.fn(async (key) => {
417
+ const keys = Array.isArray(key) ? key : [key];
418
+ for (const k of keys) {
419
+ objects.delete(k);
420
+ }
421
+ }),
422
+ list: vi.fn(async (options) => {
423
+ const prefix = options?.prefix ?? '';
424
+ const limit = options?.limit ?? 1000;
425
+ const result = [];
426
+ for (const [key, entry] of objects.entries()) {
427
+ if (key.startsWith(prefix)) {
428
+ result.push(createR2Object(key, entry));
429
+ if (result.length >= limit)
430
+ break;
431
+ }
432
+ }
433
+ return {
434
+ objects: result,
435
+ truncated: result.length >= limit,
436
+ cursor: result.length >= limit ? 'next-cursor' : undefined,
437
+ delimitedPrefixes: []
438
+ };
439
+ }),
440
+ createMultipartUpload: vi.fn(),
441
+ resumeMultipartUpload: vi.fn()
442
+ };
443
+ return bucket;
444
+ }
445
+ // ============================================================================
446
+ // Test Utilities
447
+ // ============================================================================
448
+ /**
449
+ * Seed a mock D1 database with initial data
450
+ */
451
+ export function seedMockD1(db, tableName, rows) {
452
+ db._tables.set(tableName, [...rows]);
453
+ }
454
+ /**
455
+ * Clear all data from a mock D1 database
456
+ */
457
+ export function clearMockD1(db) {
458
+ db._tables.clear();
459
+ }
460
+ /**
461
+ * Advance time for KV expiration testing
462
+ */
463
+ export function advanceKVTime(kv, ms) {
464
+ // Modify expiration times to simulate time passage
465
+ for (const [key, entry] of kv._store.entries()) {
466
+ if (entry.expiresAt) {
467
+ entry.expiresAt -= ms;
468
+ }
469
+ }
470
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Cache Service - KV Key-Value Store Abstraction
3
+ *
4
+ * Provides typed caching operations with:
5
+ * - Automatic JSON serialization/deserialization
6
+ * - TTL management with sensible defaults
7
+ * - Namespace prefixing to avoid key collisions
8
+ * - Compute-if-missing pattern (getOrSet)
9
+ * - Specific error types for debugging
10
+ */
11
+ export interface CacheOptions {
12
+ /** Time-to-live in seconds (default: 3600 = 1 hour) */
13
+ ttl?: number;
14
+ /** Namespace prefix for the key */
15
+ namespace?: string;
16
+ }
17
+ export interface GetOrSetOptions<T> extends CacheOptions {
18
+ /** Function to compute the value if not in cache */
19
+ compute: () => Promise<T>;
20
+ /** Skip cache read and always compute fresh (useful for cache invalidation) */
21
+ forceRefresh?: boolean;
22
+ }
23
+ export declare class CacheError extends Error {
24
+ readonly code: CacheErrorCode;
25
+ readonly cause?: unknown | undefined;
26
+ constructor(message: string, code: CacheErrorCode, cause?: unknown | undefined);
27
+ }
28
+ export type CacheErrorCode = 'GET_FAILED' | 'SET_FAILED' | 'DELETE_FAILED' | 'SERIALIZATION_ERROR' | 'COMPUTE_FAILED' | 'KV_UNAVAILABLE';
29
+ /**
30
+ * Get a value from the cache
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const user = await cache.get<User>(kv, 'user:123');
35
+ * if (user) {
36
+ * console.log(user.name);
37
+ * }
38
+ * ```
39
+ */
40
+ export declare function get<T>(kv: KVNamespace, key: string, options?: Pick<CacheOptions, 'namespace'>): Promise<T | null>;
41
+ /**
42
+ * Set a value in the cache
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * await cache.set(kv, 'user:123', userData, { ttl: 3600 });
47
+ * ```
48
+ */
49
+ export declare function set<T>(kv: KVNamespace, key: string, value: T, options?: CacheOptions): Promise<void>;
50
+ /**
51
+ * Delete a value from the cache
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * await cache.del(kv, 'user:123');
56
+ * ```
57
+ */
58
+ export declare function del(kv: KVNamespace, key: string, options?: Pick<CacheOptions, 'namespace'>): Promise<void>;
59
+ /**
60
+ * Get a value from cache, or compute and store it if missing
61
+ * This is the most common caching pattern.
62
+ *
63
+ * NOTE: Cache writes are fire-and-forget for better response time. This means
64
+ * subsequent requests might miss the cache briefly until the write completes.
65
+ * Use `getOrSetSync` if you need to ensure the value is cached before returning
66
+ * (e.g., when cache consistency is critical).
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * const user = await cache.getOrSet(kv, `user:${id}`, {
71
+ * ttl: 3600,
72
+ * compute: async () => {
73
+ * return await db.queryOne<User>('SELECT * FROM users WHERE id = ?', [id]);
74
+ * }
75
+ * });
76
+ * ```
77
+ */
78
+ export declare function getOrSet<T>(kv: KVNamespace, key: string, options: GetOrSetOptions<T>): Promise<T>;
79
+ /**
80
+ * Get a value from cache, or compute and store it (awaiting the cache write)
81
+ * Use this when you need to ensure the value is cached before returning.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const user = await cache.getOrSetSync(kv, `user:${id}`, {
86
+ * ttl: 3600,
87
+ * compute: async () => fetchUserFromApi(id)
88
+ * });
89
+ * ```
90
+ */
91
+ export declare function getOrSetSync<T>(kv: KVNamespace, key: string, options: GetOrSetOptions<T>): Promise<T>;
92
+ /**
93
+ * Delete multiple keys at once
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * await cache.delMany(kv, ['user:1', 'user:2', 'user:3']);
98
+ * ```
99
+ */
100
+ export declare function delMany(kv: KVNamespace, keys: string[], options?: Pick<CacheOptions, 'namespace'>): Promise<void>;
101
+ /**
102
+ * Delete all keys matching a pattern (by listing and deleting)
103
+ * Note: KV list operations have pagination limits
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * await cache.delByPrefix(kv, 'user:'); // Deletes all user cache entries
108
+ * ```
109
+ */
110
+ export declare function delByPrefix(kv: KVNamespace, prefix: string, options?: Pick<CacheOptions, 'namespace'>): Promise<number>;
111
+ /**
112
+ * Check if a key exists in the cache
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * if (await cache.has(kv, 'user:123')) {
117
+ * // Key exists
118
+ * }
119
+ * ```
120
+ */
121
+ export declare function has(kv: KVNamespace, key: string, options?: Pick<CacheOptions, 'namespace'>): Promise<boolean>;
122
+ /**
123
+ * Touch a key (refresh its TTL without changing value)
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * await cache.touch(kv, 'session:abc', { ttl: 3600 });
128
+ * ```
129
+ */
130
+ export declare function touch(kv: KVNamespace, key: string, options?: CacheOptions): Promise<boolean>;
131
+ /**
132
+ * Simple rate limiting using KV
133
+ * Returns true if the action is allowed, false if rate limited
134
+ *
135
+ * LIMITATION: This implementation has a read-modify-write pattern that is not
136
+ * atomic. Under high concurrency, multiple requests may exceed the limit slightly.
137
+ * For precise rate limiting in high-traffic scenarios, consider using Cloudflare
138
+ * Durable Objects or an external rate limiting service.
139
+ *
140
+ * For most use cases (login attempts, API throttling), this is sufficient as
141
+ * slight over-allowance is acceptable.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * // Allow 5 login attempts per 15 minutes
146
+ * const result = await cache.rateLimit(kv, `login:${email}`, {
147
+ * limit: 5,
148
+ * windowSeconds: 900
149
+ * });
150
+ * if (!result.allowed) {
151
+ * throw new Error('Too many attempts');
152
+ * }
153
+ * ```
154
+ */
155
+ export declare function rateLimit(kv: KVNamespace, key: string, options: {
156
+ /** Maximum number of attempts allowed */
157
+ limit: number;
158
+ /** Time window in seconds */
159
+ windowSeconds: number;
160
+ /** Namespace for the rate limit key */
161
+ namespace?: string;
162
+ }): Promise<{
163
+ allowed: boolean;
164
+ remaining: number;
165
+ resetAt: number;
166
+ }>;
167
+ export declare const CACHE_DEFAULTS: {
168
+ readonly TTL_SECONDS: 3600;
169
+ readonly KEY_PREFIX: "grove";
170
+ };