@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,335 @@
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
+ // ============================================================================
12
+ // Errors
13
+ // ============================================================================
14
+ export class CacheError extends Error {
15
+ code;
16
+ cause;
17
+ constructor(message, code, cause) {
18
+ super(message);
19
+ this.code = code;
20
+ this.cause = cause;
21
+ this.name = 'CacheError';
22
+ }
23
+ }
24
+ // ============================================================================
25
+ // Configuration
26
+ // ============================================================================
27
+ /** Default TTL: 1 hour */
28
+ const DEFAULT_TTL_SECONDS = 3600;
29
+ /** Key prefix to avoid collisions with other KV users */
30
+ const KEY_PREFIX = 'grove';
31
+ // ============================================================================
32
+ // Key Management
33
+ // ============================================================================
34
+ /**
35
+ * Build a namespaced cache key
36
+ * @example buildKey('user', '123') => 'grove:user:123'
37
+ */
38
+ function buildKey(namespace, key) {
39
+ const parts = [KEY_PREFIX];
40
+ if (namespace)
41
+ parts.push(namespace);
42
+ parts.push(key);
43
+ return parts.join(':');
44
+ }
45
+ // ============================================================================
46
+ // Cache Operations
47
+ // ============================================================================
48
+ /**
49
+ * Get a value from the cache
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const user = await cache.get<User>(kv, 'user:123');
54
+ * if (user) {
55
+ * console.log(user.name);
56
+ * }
57
+ * ```
58
+ */
59
+ export async function get(kv, key, options) {
60
+ const fullKey = buildKey(options?.namespace, key);
61
+ try {
62
+ const value = await kv.get(fullKey, 'text');
63
+ if (value === null) {
64
+ return null;
65
+ }
66
+ try {
67
+ return JSON.parse(value);
68
+ }
69
+ catch {
70
+ // If it's not JSON, return as-is (for string values)
71
+ return value;
72
+ }
73
+ }
74
+ catch (err) {
75
+ throw new CacheError(`Failed to get key: ${fullKey}`, 'GET_FAILED', err);
76
+ }
77
+ }
78
+ /**
79
+ * Set a value in the cache
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * await cache.set(kv, 'user:123', userData, { ttl: 3600 });
84
+ * ```
85
+ */
86
+ export async function set(kv, key, value, options) {
87
+ const fullKey = buildKey(options?.namespace, key);
88
+ const ttl = options?.ttl ?? DEFAULT_TTL_SECONDS;
89
+ let serialized;
90
+ try {
91
+ serialized = typeof value === 'string' ? value : JSON.stringify(value);
92
+ }
93
+ catch (err) {
94
+ throw new CacheError('Failed to serialize value', 'SERIALIZATION_ERROR', err);
95
+ }
96
+ try {
97
+ await kv.put(fullKey, serialized, {
98
+ expirationTtl: ttl
99
+ });
100
+ }
101
+ catch (err) {
102
+ throw new CacheError(`Failed to set key: ${fullKey}`, 'SET_FAILED', err);
103
+ }
104
+ }
105
+ /**
106
+ * Delete a value from the cache
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * await cache.del(kv, 'user:123');
111
+ * ```
112
+ */
113
+ export async function del(kv, key, options) {
114
+ const fullKey = buildKey(options?.namespace, key);
115
+ try {
116
+ await kv.delete(fullKey);
117
+ }
118
+ catch (err) {
119
+ throw new CacheError(`Failed to delete key: ${fullKey}`, 'DELETE_FAILED', err);
120
+ }
121
+ }
122
+ /**
123
+ * Get a value from cache, or compute and store it if missing
124
+ * This is the most common caching pattern.
125
+ *
126
+ * NOTE: Cache writes are fire-and-forget for better response time. This means
127
+ * subsequent requests might miss the cache briefly until the write completes.
128
+ * Use `getOrSetSync` if you need to ensure the value is cached before returning
129
+ * (e.g., when cache consistency is critical).
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * const user = await cache.getOrSet(kv, `user:${id}`, {
134
+ * ttl: 3600,
135
+ * compute: async () => {
136
+ * return await db.queryOne<User>('SELECT * FROM users WHERE id = ?', [id]);
137
+ * }
138
+ * });
139
+ * ```
140
+ */
141
+ export async function getOrSet(kv, key, options) {
142
+ const { compute, forceRefresh, ...cacheOptions } = options;
143
+ // Try to get from cache first (unless forcing refresh)
144
+ if (!forceRefresh) {
145
+ const cached = await get(kv, key, cacheOptions);
146
+ if (cached !== null) {
147
+ return cached;
148
+ }
149
+ }
150
+ // Compute the value
151
+ let value;
152
+ try {
153
+ value = await compute();
154
+ }
155
+ catch (err) {
156
+ throw new CacheError('Failed to compute value', 'COMPUTE_FAILED', err);
157
+ }
158
+ // Store in cache (don't await - fire and forget for performance)
159
+ // Errors here are logged but don't fail the request
160
+ set(kv, key, value, cacheOptions).catch((err) => {
161
+ console.error(`[Cache] Failed to store key ${key}:`, err);
162
+ });
163
+ return value;
164
+ }
165
+ /**
166
+ * Get a value from cache, or compute and store it (awaiting the cache write)
167
+ * Use this when you need to ensure the value is cached before returning.
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * const user = await cache.getOrSetSync(kv, `user:${id}`, {
172
+ * ttl: 3600,
173
+ * compute: async () => fetchUserFromApi(id)
174
+ * });
175
+ * ```
176
+ */
177
+ export async function getOrSetSync(kv, key, options) {
178
+ const { compute, forceRefresh, ...cacheOptions } = options;
179
+ // Try to get from cache first (unless forcing refresh)
180
+ if (!forceRefresh) {
181
+ const cached = await get(kv, key, cacheOptions);
182
+ if (cached !== null) {
183
+ return cached;
184
+ }
185
+ }
186
+ // Compute the value
187
+ let value;
188
+ try {
189
+ value = await compute();
190
+ }
191
+ catch (err) {
192
+ throw new CacheError('Failed to compute value', 'COMPUTE_FAILED', err);
193
+ }
194
+ // Store in cache and wait for it
195
+ await set(kv, key, value, cacheOptions);
196
+ return value;
197
+ }
198
+ // ============================================================================
199
+ // Batch Operations
200
+ // ============================================================================
201
+ /**
202
+ * Delete multiple keys at once
203
+ *
204
+ * @example
205
+ * ```ts
206
+ * await cache.delMany(kv, ['user:1', 'user:2', 'user:3']);
207
+ * ```
208
+ */
209
+ export async function delMany(kv, keys, options) {
210
+ await Promise.all(keys.map((key) => del(kv, key, options)));
211
+ }
212
+ /**
213
+ * Delete all keys matching a pattern (by listing and deleting)
214
+ * Note: KV list operations have pagination limits
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * await cache.delByPrefix(kv, 'user:'); // Deletes all user cache entries
219
+ * ```
220
+ */
221
+ export async function delByPrefix(kv, prefix, options) {
222
+ const fullPrefix = buildKey(options?.namespace, prefix);
223
+ let deleted = 0;
224
+ let cursor;
225
+ do {
226
+ const list = await kv.list({ prefix: fullPrefix, cursor });
227
+ const keys = list.keys.map((k) => k.name);
228
+ if (keys.length > 0) {
229
+ await Promise.all(keys.map((key) => kv.delete(key)));
230
+ deleted += keys.length;
231
+ }
232
+ cursor = list.list_complete ? undefined : list.cursor;
233
+ } while (cursor);
234
+ return deleted;
235
+ }
236
+ // ============================================================================
237
+ // Utility Functions
238
+ // ============================================================================
239
+ /**
240
+ * Check if a key exists in the cache
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * if (await cache.has(kv, 'user:123')) {
245
+ * // Key exists
246
+ * }
247
+ * ```
248
+ */
249
+ export async function has(kv, key, options) {
250
+ const value = await get(kv, key, options);
251
+ return value !== null;
252
+ }
253
+ /**
254
+ * Touch a key (refresh its TTL without changing value)
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * await cache.touch(kv, 'session:abc', { ttl: 3600 });
259
+ * ```
260
+ */
261
+ export async function touch(kv, key, options) {
262
+ const value = await get(kv, key, options);
263
+ if (value === null) {
264
+ return false;
265
+ }
266
+ await set(kv, key, value, options);
267
+ return true;
268
+ }
269
+ // ============================================================================
270
+ // Rate Limiting Helpers
271
+ // ============================================================================
272
+ /**
273
+ * Simple rate limiting using KV
274
+ * Returns true if the action is allowed, false if rate limited
275
+ *
276
+ * LIMITATION: This implementation has a read-modify-write pattern that is not
277
+ * atomic. Under high concurrency, multiple requests may exceed the limit slightly.
278
+ * For precise rate limiting in high-traffic scenarios, consider using Cloudflare
279
+ * Durable Objects or an external rate limiting service.
280
+ *
281
+ * For most use cases (login attempts, API throttling), this is sufficient as
282
+ * slight over-allowance is acceptable.
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * // Allow 5 login attempts per 15 minutes
287
+ * const result = await cache.rateLimit(kv, `login:${email}`, {
288
+ * limit: 5,
289
+ * windowSeconds: 900
290
+ * });
291
+ * if (!result.allowed) {
292
+ * throw new Error('Too many attempts');
293
+ * }
294
+ * ```
295
+ */
296
+ export async function rateLimit(kv, key, options) {
297
+ const fullKey = buildKey(options.namespace ?? 'ratelimit', key);
298
+ // Get current count
299
+ const data = await get(kv, fullKey);
300
+ const now = Math.floor(Date.now() / 1000);
301
+ // If no data or window expired, start fresh
302
+ if (!data || data.resetAt <= now) {
303
+ const resetAt = now + options.windowSeconds;
304
+ await set(kv, fullKey, { count: 1, resetAt }, { ttl: options.windowSeconds });
305
+ return {
306
+ allowed: true,
307
+ remaining: options.limit - 1,
308
+ resetAt
309
+ };
310
+ }
311
+ // Check if over limit
312
+ if (data.count >= options.limit) {
313
+ return {
314
+ allowed: false,
315
+ remaining: 0,
316
+ resetAt: data.resetAt
317
+ };
318
+ }
319
+ // Increment count
320
+ const newCount = data.count + 1;
321
+ const remaining = Math.max(0, options.limit - newCount);
322
+ await set(kv, fullKey, { count: newCount, resetAt: data.resetAt }, { ttl: data.resetAt - now });
323
+ return {
324
+ allowed: true,
325
+ remaining,
326
+ resetAt: data.resetAt
327
+ };
328
+ }
329
+ // ============================================================================
330
+ // Constants Export
331
+ // ============================================================================
332
+ export const CACHE_DEFAULTS = {
333
+ TTL_SECONDS: DEFAULT_TTL_SECONDS,
334
+ KEY_PREFIX
335
+ };
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Database Service - D1 SQLite Abstraction
3
+ *
4
+ * Provides typed utilities for D1 database operations:
5
+ * - Type-safe query helpers
6
+ * - Transaction/batch support
7
+ * - Specific error types for debugging
8
+ * - Common utility functions
9
+ *
10
+ * Note: Domain-specific operations (users, sessions, etc.) remain in
11
+ * their respective packages. This module provides shared primitives.
12
+ */
13
+ /**
14
+ * Type alias for D1 database or session
15
+ * Allows functions to accept either a raw database or a session-scoped connection
16
+ */
17
+ export type D1DatabaseOrSession = D1Database | D1DatabaseSession;
18
+ /**
19
+ * Query result metadata from D1
20
+ */
21
+ export interface QueryMeta {
22
+ /** Number of rows changed (for INSERT/UPDATE/DELETE) */
23
+ changes: number;
24
+ /** Duration of the query in milliseconds */
25
+ duration: number;
26
+ /** Last inserted row ID (SQLite) */
27
+ lastRowId: number;
28
+ /** Number of rows read */
29
+ rowsRead: number;
30
+ /** Number of rows written */
31
+ rowsWritten: number;
32
+ }
33
+ /**
34
+ * Result from an execute (non-SELECT) operation
35
+ */
36
+ export interface ExecuteResult {
37
+ success: boolean;
38
+ meta: QueryMeta;
39
+ }
40
+ export declare class DatabaseError extends Error {
41
+ readonly code: DatabaseErrorCode;
42
+ readonly cause?: unknown | undefined;
43
+ constructor(message: string, code: DatabaseErrorCode, cause?: unknown | undefined);
44
+ }
45
+ export type DatabaseErrorCode = 'QUERY_FAILED' | 'NOT_FOUND' | 'CONSTRAINT_VIOLATION' | 'TRANSACTION_FAILED' | 'CONNECTION_ERROR' | 'INVALID_QUERY';
46
+ /**
47
+ * Generate a UUID v4 identifier
48
+ */
49
+ export declare function generateId(): string;
50
+ /**
51
+ * Get current timestamp in ISO format
52
+ */
53
+ export declare function now(): string;
54
+ /**
55
+ * Get a timestamp for a future time
56
+ * @param ms - Milliseconds from now
57
+ */
58
+ export declare function futureTimestamp(ms: number): string;
59
+ /**
60
+ * Check if a timestamp has expired
61
+ */
62
+ export declare function isExpired(timestamp: string): boolean;
63
+ /**
64
+ * Execute a query expecting a single row result
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const user = await db.queryOne<User>(
69
+ * db,
70
+ * 'SELECT * FROM users WHERE id = ?',
71
+ * [userId]
72
+ * );
73
+ * if (!user) throw new Error('User not found');
74
+ * ```
75
+ */
76
+ export declare function queryOne<T>(db: D1DatabaseOrSession, sql: string, params?: unknown[]): Promise<T | null>;
77
+ /**
78
+ * Execute a query expecting a single row, throw if not found
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const user = await db.queryOneOrThrow<User>(
83
+ * db,
84
+ * 'SELECT * FROM users WHERE id = ?',
85
+ * [userId]
86
+ * );
87
+ * // user is guaranteed to exist here
88
+ * ```
89
+ */
90
+ export declare function queryOneOrThrow<T>(db: D1DatabaseOrSession, sql: string, params?: unknown[], errorMessage?: string): Promise<T>;
91
+ /**
92
+ * Execute a query expecting multiple rows
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * const users = await db.queryMany<User>(
97
+ * db,
98
+ * 'SELECT * FROM users WHERE is_admin = ?',
99
+ * [1]
100
+ * );
101
+ * ```
102
+ */
103
+ export declare function queryMany<T>(db: D1DatabaseOrSession, sql: string, params?: unknown[]): Promise<T[]>;
104
+ /**
105
+ * Execute a non-SELECT statement (INSERT, UPDATE, DELETE)
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const result = await db.execute(
110
+ * db,
111
+ * 'UPDATE users SET name = ? WHERE id = ?',
112
+ * [newName, userId]
113
+ * );
114
+ * console.log(`Updated ${result.meta.changes} rows`);
115
+ * ```
116
+ */
117
+ export declare function execute(db: D1DatabaseOrSession, sql: string, params?: unknown[]): Promise<ExecuteResult>;
118
+ /**
119
+ * Execute a statement and throw if no rows were affected
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * await db.executeOrThrow(
124
+ * db,
125
+ * 'DELETE FROM sessions WHERE id = ?',
126
+ * [sessionId],
127
+ * 'Session not found'
128
+ * );
129
+ * ```
130
+ */
131
+ export declare function executeOrThrow(db: D1DatabaseOrSession, sql: string, params?: unknown[], errorMessage?: string): Promise<ExecuteResult>;
132
+ /**
133
+ * Execute multiple statements atomically
134
+ * All statements succeed or all fail (D1 batch semantics)
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * await db.batch(db, [
139
+ * { sql: 'INSERT INTO users (id, email) VALUES (?, ?)', params: [id, email] },
140
+ * { sql: 'INSERT INTO profiles (user_id) VALUES (?)', params: [id] }
141
+ * ]);
142
+ * ```
143
+ */
144
+ export declare function batch(db: D1Database, statements: Array<{
145
+ sql: string;
146
+ params?: unknown[];
147
+ }>): Promise<ExecuteResult[]>;
148
+ /**
149
+ * Execute a function within a database session for read consistency
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * const result = await db.withSession(db, async (session) => {
154
+ * const user = await queryOne<User>(session, 'SELECT * FROM users WHERE id = ?', [id]);
155
+ * const posts = await queryMany<Post>(session, 'SELECT * FROM posts WHERE user_id = ?', [id]);
156
+ * return { user, posts };
157
+ * });
158
+ * ```
159
+ */
160
+ export declare function withSession<T>(db: D1Database, fn: (session: D1DatabaseSession) => Promise<T>): Promise<T>;
161
+ /**
162
+ * Insert a row and return the generated ID
163
+ *
164
+ * SECURITY: Table and column names are validated to prevent SQL injection.
165
+ * Only use with hardcoded names, never user input.
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * const id = await db.insert(db, 'users', {
170
+ * email: 'user@example.com',
171
+ * name: 'John'
172
+ * });
173
+ * ```
174
+ */
175
+ export declare function insert(db: D1DatabaseOrSession, table: string, data: Record<string, unknown>, options?: {
176
+ id?: string;
177
+ }): Promise<string>;
178
+ /**
179
+ * Update rows matching a condition
180
+ *
181
+ * SECURITY: Table and column names are validated to prevent SQL injection.
182
+ * Only use with hardcoded names, never user input.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * const changes = await db.update(db, 'users', { name: 'Jane' }, 'id = ?', [userId]);
187
+ * ```
188
+ */
189
+ export declare function update(db: D1DatabaseOrSession, table: string, data: Record<string, unknown>, where: string, whereParams?: unknown[]): Promise<number>;
190
+ /**
191
+ * Delete rows matching a condition
192
+ *
193
+ * SECURITY: Table name is validated to prevent SQL injection.
194
+ * Only use with hardcoded table names, never user input.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * const deleted = await db.deleteWhere(db, 'sessions', 'expires_at < datetime("now")');
199
+ * ```
200
+ */
201
+ export declare function deleteWhere(db: D1DatabaseOrSession, table: string, where: string, whereParams?: unknown[]): Promise<number>;
202
+ /**
203
+ * Delete a single row by ID
204
+ *
205
+ * @example
206
+ * ```ts
207
+ * await db.deleteById(db, 'users', userId);
208
+ * ```
209
+ */
210
+ export declare function deleteById(db: D1DatabaseOrSession, table: string, id: string): Promise<boolean>;
211
+ /**
212
+ * Check if a row exists
213
+ *
214
+ * SECURITY: Table name is validated to prevent SQL injection.
215
+ * Only use with hardcoded table names, never user input.
216
+ *
217
+ * @example
218
+ * ```ts
219
+ * if (await db.exists(db, 'users', 'email = ?', [email])) {
220
+ * throw new Error('Email already registered');
221
+ * }
222
+ * ```
223
+ */
224
+ export declare function exists(db: D1DatabaseOrSession, table: string, where: string, whereParams?: unknown[]): Promise<boolean>;
225
+ /**
226
+ * Count rows matching a condition
227
+ *
228
+ * SECURITY: Table name is validated to prevent SQL injection.
229
+ * Only use with hardcoded table names, never user input.
230
+ *
231
+ * @example
232
+ * ```ts
233
+ * const activeUsers = await db.count(db, 'users', 'is_active = ?', [1]);
234
+ * ```
235
+ */
236
+ export declare function count(db: D1DatabaseOrSession, table: string, where?: string, whereParams?: unknown[]): Promise<number>;