@geenius/tools 0.1.0

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 (160) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.env.example +2 -0
  3. package/.github/CODEOWNERS +1 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
  7. package/.github/dependabot.yml +11 -0
  8. package/.github/workflows/ci.yml +23 -0
  9. package/.github/workflows/release.yml +29 -0
  10. package/.node-version +1 -0
  11. package/.nvmrc +1 -0
  12. package/.prettierrc +7 -0
  13. package/.project/ACCOUNT.yaml +4 -0
  14. package/.project/IDEAS.yaml +7 -0
  15. package/.project/PROJECT.yaml +11 -0
  16. package/.project/ROADMAP.yaml +15 -0
  17. package/CHANGELOG.md +16 -0
  18. package/CODE_OF_CONDUCT.md +26 -0
  19. package/CONTRIBUTING.md +69 -0
  20. package/LICENSE +21 -0
  21. package/README.md +1 -0
  22. package/SECURITY.md +18 -0
  23. package/SUPPORT.md +14 -0
  24. package/package.json +75 -0
  25. package/packages/convex/shared/README.md +1 -0
  26. package/packages/convex/shared/package.json +42 -0
  27. package/packages/convex/shared/src/audit/index.ts +5 -0
  28. package/packages/convex/shared/src/audit/presets.ts +165 -0
  29. package/packages/convex/shared/src/audit/schema.ts +85 -0
  30. package/packages/convex/shared/src/audit/write.ts +102 -0
  31. package/packages/convex/shared/src/extract.ts +75 -0
  32. package/packages/convex/shared/src/index.ts +41 -0
  33. package/packages/convex/shared/src/messages.ts +45 -0
  34. package/packages/convex/shared/src/security.ts +112 -0
  35. package/packages/convex/shared/src/throw.ts +184 -0
  36. package/packages/convex/shared/src/types.ts +57 -0
  37. package/packages/convex/shared/src/utils.ts +58 -0
  38. package/packages/convex/shared/tsconfig.json +28 -0
  39. package/packages/convex/shared/tsup.config.ts +12 -0
  40. package/packages/devtools/package.json +27 -0
  41. package/packages/devtools/react/README.md +1 -0
  42. package/packages/devtools/react/package.json +53 -0
  43. package/packages/devtools/react/src/components/DesignPreview.tsx +59 -0
  44. package/packages/devtools/react/src/components/DesignSwitcherDropdown.tsx +99 -0
  45. package/packages/devtools/react/src/components/DevSidebar.tsx +247 -0
  46. package/packages/devtools/react/src/components/DevToolbar.tsx +242 -0
  47. package/packages/devtools/react/src/components/GitHubIssueDialog.tsx +402 -0
  48. package/packages/devtools/react/src/components/InspectorOverlay.tsx +312 -0
  49. package/packages/devtools/react/src/components/PageLoadWaterfall.tsx +144 -0
  50. package/packages/devtools/react/src/components/PerformancePanel.tsx +330 -0
  51. package/packages/devtools/react/src/context/DevModeContext.tsx +226 -0
  52. package/packages/devtools/react/src/context/PerformanceContext.tsx +143 -0
  53. package/packages/devtools/react/src/data/designs.ts +13 -0
  54. package/packages/devtools/react/src/hooks/useGitHubLabels.ts +47 -0
  55. package/packages/devtools/react/src/hooks/useVirtualList.ts +124 -0
  56. package/packages/devtools/react/src/index.ts +77 -0
  57. package/packages/devtools/react/src/panels/ConvexSpy.tsx +130 -0
  58. package/packages/devtools/react/src/panels/DatabaseSeeder.tsx +116 -0
  59. package/packages/devtools/react/src/panels/DevModePhase2.tsx +191 -0
  60. package/packages/devtools/react/src/panels/DevModePhase3.tsx +234 -0
  61. package/packages/devtools/react/src/panels/FeatureFlagsToggle.tsx +104 -0
  62. package/packages/devtools/react/src/panels/QuickRouteJump.tsx +152 -0
  63. package/packages/devtools/react/src/services/github-service.ts +247 -0
  64. package/packages/devtools/react/tsconfig.json +31 -0
  65. package/packages/devtools/react/tsup.config.ts +18 -0
  66. package/packages/devtools/solidjs/README.md +1 -0
  67. package/packages/devtools/solidjs/package.json +49 -0
  68. package/packages/devtools/solidjs/src/components/DesignPreview.tsx +51 -0
  69. package/packages/devtools/solidjs/src/components/DesignSwitcherDropdown.tsx +95 -0
  70. package/packages/devtools/solidjs/src/components/DevSidebar.tsx +247 -0
  71. package/packages/devtools/solidjs/src/components/DevToolbar.tsx +242 -0
  72. package/packages/devtools/solidjs/src/components/GitHubIssueDialog.tsx +400 -0
  73. package/packages/devtools/solidjs/src/components/InspectorOverlay.tsx +311 -0
  74. package/packages/devtools/solidjs/src/components/PageLoadWaterfall.tsx +144 -0
  75. package/packages/devtools/solidjs/src/components/PerformancePanel.tsx +330 -0
  76. package/packages/devtools/solidjs/src/context/DevModeContext.tsx +216 -0
  77. package/packages/devtools/solidjs/src/context/PerformanceContext.tsx +135 -0
  78. package/packages/devtools/solidjs/src/data/designs.ts +13 -0
  79. package/packages/devtools/solidjs/src/hooks/createGitHubLabels.ts +47 -0
  80. package/packages/devtools/solidjs/src/index.ts +64 -0
  81. package/packages/devtools/solidjs/src/services/github-service.ts +247 -0
  82. package/packages/devtools/solidjs/tsconfig.json +21 -0
  83. package/packages/devtools/src/index.ts +377 -0
  84. package/packages/devtools/tsup.config.ts +12 -0
  85. package/packages/env/package.json +30 -0
  86. package/packages/env/src/index.ts +264 -0
  87. package/packages/env/tsup.config.ts +12 -0
  88. package/packages/errors/package.json +27 -0
  89. package/packages/errors/react/README.md +1 -0
  90. package/packages/errors/react/package.json +72 -0
  91. package/packages/errors/react/src/analytics.ts +16 -0
  92. package/packages/errors/react/src/components/ErrorBoundary.tsx +248 -0
  93. package/packages/errors/react/src/components/ErrorDisplay.tsx +328 -0
  94. package/packages/errors/react/src/components/ValidationErrors.tsx +102 -0
  95. package/packages/errors/react/src/config.ts +199 -0
  96. package/packages/errors/react/src/constants.ts +74 -0
  97. package/packages/errors/react/src/hooks/useErrorBoundary.ts +92 -0
  98. package/packages/errors/react/src/hooks/useErrorHandler.ts +87 -0
  99. package/packages/errors/react/src/index.ts +96 -0
  100. package/packages/errors/react/src/types.ts +102 -0
  101. package/packages/errors/react/src/utils/errorMessages.ts +35 -0
  102. package/packages/errors/react/src/utils/errorPolicy.ts +139 -0
  103. package/packages/errors/react/src/utils/extractAppError.ts +174 -0
  104. package/packages/errors/react/src/utils/formatError.ts +112 -0
  105. package/packages/errors/react/tsconfig.json +25 -0
  106. package/packages/errors/react/tsup.config.ts +24 -0
  107. package/packages/errors/solidjs/README.md +1 -0
  108. package/packages/errors/solidjs/package.json +46 -0
  109. package/packages/errors/solidjs/src/components/ErrorDisplay.tsx +179 -0
  110. package/packages/errors/solidjs/src/config.ts +98 -0
  111. package/packages/errors/solidjs/src/hooks/createErrorHandler.ts +107 -0
  112. package/packages/errors/solidjs/src/index.ts +61 -0
  113. package/packages/errors/solidjs/src/types.ts +34 -0
  114. package/packages/errors/solidjs/src/utils/errorPolicy.ts +56 -0
  115. package/packages/errors/solidjs/src/utils/extractAppError.ts +94 -0
  116. package/packages/errors/solidjs/src/utils/formatError.ts +33 -0
  117. package/packages/errors/solidjs/tsconfig.json +26 -0
  118. package/packages/errors/solidjs/tsup.config.ts +21 -0
  119. package/packages/errors/src/index.ts +320 -0
  120. package/packages/errors/tsup.config.ts +12 -0
  121. package/packages/logger/package.json +27 -0
  122. package/packages/logger/react/README.md +1 -0
  123. package/packages/logger/react/package.json +46 -0
  124. package/packages/logger/react/src/index.ts +4 -0
  125. package/packages/logger/react/src/useMetrics.ts +42 -0
  126. package/packages/logger/react/src/usePerformanceLog.ts +61 -0
  127. package/packages/logger/react/tsconfig.json +31 -0
  128. package/packages/logger/react/tsup.config.ts +12 -0
  129. package/packages/logger/solidjs/README.md +1 -0
  130. package/packages/logger/solidjs/package.json +45 -0
  131. package/packages/logger/solidjs/src/createMetrics.ts +37 -0
  132. package/packages/logger/solidjs/src/createPerformanceLog.ts +58 -0
  133. package/packages/logger/solidjs/src/index.ts +4 -0
  134. package/packages/logger/solidjs/tsconfig.json +32 -0
  135. package/packages/logger/solidjs/tsup.config.ts +12 -0
  136. package/packages/logger/src/index.ts +363 -0
  137. package/packages/logger/tsup.config.ts +12 -0
  138. package/packages/perf/package.json +27 -0
  139. package/packages/perf/react/README.md +1 -0
  140. package/packages/perf/react/package.json +59 -0
  141. package/packages/perf/react/src/components/PerformanceDashboard.tsx +257 -0
  142. package/packages/perf/react/src/hooks/useMonitoredQuery.ts +89 -0
  143. package/packages/perf/react/src/hooks/usePerformanceMetrics.ts +78 -0
  144. package/packages/perf/react/src/index.ts +33 -0
  145. package/packages/perf/react/src/services/PerformanceMonitor.ts +313 -0
  146. package/packages/perf/react/src/types.ts +77 -0
  147. package/packages/perf/react/tsconfig.json +25 -0
  148. package/packages/perf/react/tsup.config.ts +19 -0
  149. package/packages/perf/solidjs/README.md +1 -0
  150. package/packages/perf/solidjs/package.json +41 -0
  151. package/packages/perf/solidjs/src/components/PerformanceDashboard.tsx +207 -0
  152. package/packages/perf/solidjs/src/hooks/createPerformanceMetrics.ts +73 -0
  153. package/packages/perf/solidjs/src/index.ts +31 -0
  154. package/packages/perf/solidjs/src/services/PerformanceMonitor.ts +134 -0
  155. package/packages/perf/solidjs/src/types.ts +78 -0
  156. package/packages/perf/solidjs/tsconfig.json +26 -0
  157. package/packages/perf/solidjs/tsup.config.ts +14 -0
  158. package/packages/perf/src/index.ts +410 -0
  159. package/packages/perf/tsup.config.ts +12 -0
  160. package/pnpm-workspace.yaml +2 -0
@@ -0,0 +1,85 @@
1
+ // @geenius-tools/convex-wrappers — src/audit/schema.ts
2
+
3
+ /**
4
+ * Audit log table schema definition for Convex.
5
+ *
6
+ * Usage: Import and spread into your Convex schema definition.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // convex/schema.ts
11
+ * import { defineSchema, defineTable } from 'convex/server'
12
+ * import { auditLogFields, auditLogIndexes } from '@geenius-tools/convex-wrappers/audit'
13
+ *
14
+ * export default defineSchema({
15
+ * auditLogs: defineTable(auditLogFields)
16
+ * .index('by_entity', auditLogIndexes.by_entity)
17
+ * .index('by_entityTable', auditLogIndexes.by_entityTable)
18
+ * .index('by_action', auditLogIndexes.by_action)
19
+ * .index('by_createdAt', auditLogIndexes.by_createdAt)
20
+ * .searchIndex('search_auditLogs', auditLogIndexes.search),
21
+ * })
22
+ * ```
23
+ */
24
+
25
+ import { v } from 'convex/values'
26
+
27
+ /**
28
+ * Field validators for the auditLogs table.
29
+ */
30
+ export const auditLogFields = {
31
+ /** The user who performed the action */
32
+ actorId: v.optional(v.id('users')),
33
+ /** Role of the actor (e.g., 'admin', 'user', 'system') */
34
+ role: v.string(),
35
+ /** Array of roles the actor had at time of action */
36
+ actorRoles: v.optional(v.array(v.string())),
37
+
38
+ /** Action that was performed (e.g., 'create', 'update', 'delete') */
39
+ action: v.string(),
40
+
41
+ /** Table the entity belongs to */
42
+ entityTable: v.string(),
43
+ /** ID of the affected entity */
44
+ entityId: v.optional(v.string()),
45
+ /** Type classification of the entity */
46
+ entityType: v.optional(v.string()),
47
+ /** Human-readable title of the entity */
48
+ entityTitle: v.optional(v.string()),
49
+
50
+ /** JSON snapshot of the entity before the action */
51
+ beforeJson: v.optional(v.string()),
52
+ /** JSON snapshot of the entity after the action */
53
+ afterJson: v.optional(v.string()),
54
+ /** JSON diff of changes */
55
+ diffJson: v.optional(v.string()),
56
+
57
+ /** Reason for the action (e.g., admin note) */
58
+ reason: v.optional(v.string()),
59
+ /** Human-readable description */
60
+ description: v.optional(v.string()),
61
+ /** Request tracing ID */
62
+ requestId: v.optional(v.string()),
63
+
64
+ /** Additional context-specific metadata */
65
+ metadata: v.optional(v.any()),
66
+
67
+ /** ISO timestamp of when the action occurred */
68
+ createdAt: v.string(),
69
+ /** Concatenated searchable text for full-text search */
70
+ searchableText: v.optional(v.string()),
71
+ }
72
+
73
+ /**
74
+ * Recommended indexes for the auditLogs table.
75
+ */
76
+ export const auditLogIndexes = {
77
+ by_entity: ['entityTable', 'entityId'] as const,
78
+ by_entityTable: ['entityTable'] as const,
79
+ by_action: ['action'] as const,
80
+ by_createdAt: ['createdAt'] as const,
81
+ search: {
82
+ searchField: 'searchableText' as const,
83
+ filterFields: ['entityTable', 'action'] as const,
84
+ },
85
+ }
@@ -0,0 +1,102 @@
1
+ // @geenius-tools/convex-wrappers — src/audit/write.ts
2
+
3
+ import type { AuditLogEntry } from '../types'
4
+
5
+ /**
6
+ * Generate searchable text from an audit log entry.
7
+ * Concatenates key fields for full-text search indexing.
8
+ */
9
+ export function generateSearchableText(params: {
10
+ action?: string
11
+ table?: string
12
+ id?: string
13
+ title?: string
14
+ description?: string
15
+ reason?: string
16
+ actorName?: string
17
+ [key: string]: unknown
18
+ }): string {
19
+ return [
20
+ params.action,
21
+ params.table,
22
+ params.id,
23
+ params.title,
24
+ params.description,
25
+ params.reason,
26
+ params.actorName,
27
+ ]
28
+ .filter(Boolean)
29
+ .join(' ')
30
+ }
31
+
32
+ /**
33
+ * Write an audit log entry to the database.
34
+ *
35
+ * This is a generic helper — you pass in your Convex MutationCtx.
36
+ * It auto-generates `createdAt` and `searchableText` if not provided.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { writeAudit } from '@geenius-tools/convex-wrappers'
41
+ *
42
+ * // Inside a Convex mutation:
43
+ * await writeAudit(ctx, {
44
+ * actorId: user._id,
45
+ * role: 'admin',
46
+ * action: 'update',
47
+ * entityTable: 'orders',
48
+ * entityId: orderId,
49
+ * entityTitle: 'Order #123',
50
+ * beforeJson: JSON.stringify(before),
51
+ * afterJson: JSON.stringify(after),
52
+ * })
53
+ * ```
54
+ */
55
+ export async function writeAudit(
56
+ ctx: { db: { insert: (table: string, doc: Record<string, unknown>) => Promise<unknown> } },
57
+ entry: Omit<AuditLogEntry, 'createdAt'> & { createdAt?: string; searchableText?: string },
58
+ ): Promise<void> {
59
+ const createdAt = entry.createdAt ?? new Date().toISOString()
60
+
61
+ const searchableText =
62
+ entry.searchableText ??
63
+ generateSearchableText({
64
+ action: entry.action,
65
+ table: entry.entityTable,
66
+ id: entry.entityId,
67
+ title: entry.entityTitle,
68
+ description: entry.description,
69
+ reason: entry.reason,
70
+ })
71
+
72
+ await ctx.db.insert('auditLogs', {
73
+ ...entry,
74
+ createdAt,
75
+ searchableText,
76
+ })
77
+ }
78
+
79
+ /**
80
+ * Compute a JSON diff between before and after objects.
81
+ * Returns an object with only the changed fields.
82
+ */
83
+ export function computeDiff(
84
+ before: Record<string, unknown> | null | undefined,
85
+ after: Record<string, unknown> | null | undefined,
86
+ ): Record<string, { from: unknown; to: unknown }> {
87
+ const diff: Record<string, { from: unknown; to: unknown }> = {}
88
+ const allKeys = new Set([
89
+ ...Object.keys(before ?? {}),
90
+ ...Object.keys(after ?? {}),
91
+ ])
92
+
93
+ for (const key of allKeys) {
94
+ const fromVal = (before as Record<string, unknown>)?.[key]
95
+ const toVal = (after as Record<string, unknown>)?.[key]
96
+ if (JSON.stringify(fromVal) !== JSON.stringify(toVal)) {
97
+ diff[key] = { from: fromVal, to: toVal }
98
+ }
99
+ }
100
+
101
+ return diff
102
+ }
@@ -0,0 +1,75 @@
1
+ // @geenius-tools/convex-errors — src/extract.ts
2
+
3
+ import { ConvexError } from 'convex/values'
4
+ import type { AppErrorCode, AppErrorPayload, ValidationError } from './types'
5
+ import { HTTP_STATUS_MAP } from './types'
6
+
7
+ /**
8
+ * Check if an error is a ConvexError with AppErrorPayload.
9
+ */
10
+ export function isAppError(error: unknown): error is ConvexError<AppErrorPayload> {
11
+ return error instanceof ConvexError && typeof (error.data as AppErrorPayload)?.code === 'string'
12
+ }
13
+
14
+ /**
15
+ * Extract AppErrorPayload from a ConvexError, or return null.
16
+ */
17
+ export function extractAppError(error: unknown): AppErrorPayload | null {
18
+ if (isAppError(error)) {
19
+ return error.data
20
+ }
21
+ return null
22
+ }
23
+
24
+ /**
25
+ * Get the error code from a ConvexError, or 'INTERNAL' as fallback.
26
+ */
27
+ export function getErrorCode(error: unknown): AppErrorCode {
28
+ const payload = extractAppError(error)
29
+ return payload?.code ?? 'INTERNAL'
30
+ }
31
+
32
+ /**
33
+ * Get the error message from a ConvexError, or a fallback string.
34
+ */
35
+ export function getErrorMsg(error: unknown, fallback = 'An unexpected error occurred'): string {
36
+ const payload = extractAppError(error)
37
+ return payload?.message ?? (error instanceof Error ? error.message : fallback)
38
+ }
39
+
40
+ /**
41
+ * Get validation errors from a ConvexError, or empty array.
42
+ */
43
+ export function getValidationErrors(error: unknown): ValidationError[] {
44
+ const payload = extractAppError(error)
45
+ return payload?.validationErrors ?? []
46
+ }
47
+
48
+ /**
49
+ * Get HTTP status code for a ConvexError.
50
+ */
51
+ export function getHttpStatus(error: unknown): number {
52
+ const code = getErrorCode(error)
53
+ return HTTP_STATUS_MAP[code] ?? 500
54
+ }
55
+
56
+ /**
57
+ * Convert an error to a JSON-serializable response object.
58
+ * Useful for HTTP error responses.
59
+ */
60
+ export function toErrorResponse(error: unknown): {
61
+ status: number
62
+ body: { error: AppErrorPayload }
63
+ } {
64
+ const payload = extractAppError(error)
65
+ const code = payload?.code ?? 'INTERNAL'
66
+ return {
67
+ status: HTTP_STATUS_MAP[code] ?? 500,
68
+ body: {
69
+ error: payload ?? {
70
+ code: 'INTERNAL',
71
+ message: error instanceof Error ? error.message : 'Unknown error',
72
+ },
73
+ },
74
+ }
75
+ }
@@ -0,0 +1,41 @@
1
+ // @geenius-tools/convex-errors — src/index.ts
2
+
3
+ // Types
4
+ export type {
5
+ AppErrorCode,
6
+ AppErrorPayload,
7
+ ValidationError,
8
+ } from './types'
9
+ export { HTTP_STATUS_MAP } from './types'
10
+
11
+ // Error throwers
12
+ export {
13
+ unauthenticated,
14
+ forbidden,
15
+ notFound,
16
+ badRequest,
17
+ conflict,
18
+ internal,
19
+ featureDisabled,
20
+ rateLimited,
21
+ validationError,
22
+ withErrorMeta,
23
+ } from './throw'
24
+
25
+ // Error messages
26
+ export {
27
+ DEFAULT_ERROR_MESSAGES,
28
+ configureErrorMessages,
29
+ getErrorMessage,
30
+ } from './messages'
31
+
32
+ // Error extraction (for frontend)
33
+ export {
34
+ isAppError,
35
+ extractAppError,
36
+ getErrorCode,
37
+ getErrorMsg,
38
+ getValidationErrors,
39
+ getHttpStatus,
40
+ toErrorResponse,
41
+ } from './extract'
@@ -0,0 +1,45 @@
1
+ // @geenius-tools/convex-errors — src/messages.ts
2
+
3
+ import type { AppErrorCode } from './types'
4
+
5
+ /**
6
+ * Default English error messages.
7
+ * Override these by passing your own messages to `configureErrorMessages()`.
8
+ */
9
+ export const DEFAULT_ERROR_MESSAGES: Record<AppErrorCode, string> = {
10
+ UNAUTHENTICATED: 'Please sign in to continue.',
11
+ FORBIDDEN: 'You do not have permission for this action.',
12
+ NOT_FOUND: 'The requested resource was not found.',
13
+ BAD_REQUEST: 'Invalid request. Please check your input.',
14
+ CONFLICT: 'Action not possible (conflict with existing data).',
15
+ INTERNAL: 'An internal error occurred. Please try again later.',
16
+ VALIDATION_ERROR: 'Please check your input and try again.',
17
+ RATE_LIMITED: 'Too many requests. Please try again later.',
18
+ FEATURE_DISABLED: 'This feature is currently disabled.',
19
+ }
20
+
21
+ let currentMessages: Record<AppErrorCode, string> = { ...DEFAULT_ERROR_MESSAGES }
22
+
23
+ /**
24
+ * Override default error messages (e.g., for i18n / localization).
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * configureErrorMessages({
29
+ * UNAUTHENTICATED: 'Bitte melde dich an, um fortzufahren.',
30
+ * FORBIDDEN: 'Du hast keine Berechtigung für diese Aktion.',
31
+ * })
32
+ * ```
33
+ */
34
+ export function configureErrorMessages(
35
+ messages: Partial<Record<AppErrorCode, string>>,
36
+ ): void {
37
+ currentMessages = { ...currentMessages, ...messages }
38
+ }
39
+
40
+ /**
41
+ * Get the current error message for a given code.
42
+ */
43
+ export function getErrorMessage(code: AppErrorCode): string {
44
+ return currentMessages[code]
45
+ }
@@ -0,0 +1,112 @@
1
+ // @geenius-tools/convex-wrappers — src/security.ts
2
+
3
+ import type { RateLimitConfig, PrivacyConfig } from './types'
4
+
5
+ /**
6
+ * Simple in-memory rate limiter for development/prototyping.
7
+ * In production, you'd want to use Convex's built-in rate limiting
8
+ * or a persistent store.
9
+ *
10
+ * This provides the core rate limit checking logic.
11
+ * The actual Convex integration is left to the consumer to wire up.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { checkRateLimit } from '@geenius-tools/convex-wrappers'
16
+ *
17
+ * // In your mutation wrapper:
18
+ * const isAllowed = checkRateLimit({
19
+ * key: userId,
20
+ * maxRequests: 10,
21
+ * windowMs: 60_000,
22
+ * })
23
+ * if (!isAllowed) {
24
+ * rateLimited('Too many requests')
25
+ * }
26
+ * ```
27
+ */
28
+ const rateLimitStore = new Map<string, { count: number; resetAt: number }>()
29
+
30
+ export function checkRateLimit(params: {
31
+ key: string
32
+ maxRequests: number
33
+ windowMs: number
34
+ }): boolean {
35
+ const { key, maxRequests, windowMs } = params
36
+ const now = Date.now()
37
+ const entry = rateLimitStore.get(key)
38
+
39
+ if (!entry || now > entry.resetAt) {
40
+ rateLimitStore.set(key, { count: 1, resetAt: now + windowMs })
41
+ return true
42
+ }
43
+
44
+ if (entry.count >= maxRequests) {
45
+ return false
46
+ }
47
+
48
+ entry.count++
49
+ return true
50
+ }
51
+
52
+ /**
53
+ * Clear rate limit for a specific key (e.g., after successful auth).
54
+ */
55
+ export function clearRateLimit(key: string): void {
56
+ rateLimitStore.delete(key)
57
+ }
58
+
59
+ /**
60
+ * Validate that a user has one of the required roles.
61
+ */
62
+ export function requireRoles(
63
+ user: { role?: string; roleType?: string; roles?: string[] },
64
+ requiredRoles: readonly string[],
65
+ ): void {
66
+ const userRoles = [
67
+ user.role,
68
+ user.roleType,
69
+ ...(user.roles ?? []),
70
+ ].filter(Boolean) as string[]
71
+
72
+ const hasRole = requiredRoles.some((r) => userRoles.includes(r))
73
+ if (!hasRole) {
74
+ throw new Error(
75
+ `Forbidden: user has roles [${userRoles.join(', ')}] but needs one of [${requiredRoles.join(', ')}]`,
76
+ )
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Redact args based on privacy config.
82
+ */
83
+ export function redactArgs(
84
+ args: Record<string, unknown>,
85
+ privacy: PrivacyConfig,
86
+ ): Record<string, unknown> {
87
+ if (privacy.mode === 'none') return args
88
+
89
+ const sensitive = new Set(
90
+ privacy.sensitiveFields ?? ['password', 'token', 'secret', 'apiKey'],
91
+ )
92
+
93
+ const result: Record<string, unknown> = {}
94
+ for (const [key, value] of Object.entries(args)) {
95
+ if (sensitive.has(key)) {
96
+ result[key] = privacy.mode === 'hash' ? `[HASHED:${hashSimple(String(value))}]` : '[REDACTED]'
97
+ } else {
98
+ result[key] = value
99
+ }
100
+ }
101
+ return result
102
+ }
103
+
104
+ function hashSimple(input: string): string {
105
+ let hash = 0
106
+ for (let i = 0; i < input.length; i++) {
107
+ const char = input.charCodeAt(i)
108
+ hash = (hash << 5) - hash + char
109
+ hash |= 0
110
+ }
111
+ return Math.abs(hash).toString(36)
112
+ }
@@ -0,0 +1,184 @@
1
+ // @geenius-tools/convex-errors — src/throw.ts
2
+
3
+ import { ConvexError, type Value } from 'convex/values'
4
+ import type { AppErrorCode, AppErrorPayload, ValidationError } from './types'
5
+ import { getErrorMessage } from './messages'
6
+
7
+ /**
8
+ * Internal function to throw a ConvexError with AppErrorPayload.
9
+ */
10
+ function throwAppError(
11
+ code: AppErrorCode,
12
+ message?: string,
13
+ details?: Record<string, Value>,
14
+ validationErrors?: ValidationError[],
15
+ metadata?: AppErrorPayload['_metadata'],
16
+ ): never {
17
+ const payload: AppErrorPayload = {
18
+ code,
19
+ message: message || getErrorMessage(code),
20
+ details,
21
+ validationErrors,
22
+ _metadata: {
23
+ timestamp: new Date().toISOString(),
24
+ ...metadata,
25
+ },
26
+ }
27
+ throw new ConvexError<AppErrorPayload>(payload)
28
+ }
29
+
30
+ /**
31
+ * Throw UNAUTHENTICATED error (401).
32
+ * Use when user is not authenticated.
33
+ */
34
+ export function unauthenticated(
35
+ message?: string,
36
+ details?: Record<string, Value>,
37
+ ): never {
38
+ throwAppError('UNAUTHENTICATED', message, details)
39
+ }
40
+
41
+ /**
42
+ * Throw FORBIDDEN error (403).
43
+ * Use when user lacks permission for the action.
44
+ */
45
+ export function forbidden(
46
+ message?: string,
47
+ details?: Record<string, Value>,
48
+ ): never {
49
+ throwAppError('FORBIDDEN', message, details)
50
+ }
51
+
52
+ /**
53
+ * Throw NOT_FOUND error (404).
54
+ * Use when a requested resource doesn't exist.
55
+ */
56
+ export function notFound(
57
+ message?: string,
58
+ details?: Record<string, Value>,
59
+ ): never {
60
+ throwAppError('NOT_FOUND', message, details)
61
+ }
62
+
63
+ /**
64
+ * Throw BAD_REQUEST error (400).
65
+ * Use for invalid input or business rule violations.
66
+ */
67
+ export function badRequest(
68
+ message?: string,
69
+ details?: Record<string, Value>,
70
+ ): never {
71
+ throwAppError('BAD_REQUEST', message, details)
72
+ }
73
+
74
+ /**
75
+ * Throw CONFLICT error (409).
76
+ * Use for state conflicts (e.g., optimistic concurrency control failures).
77
+ */
78
+ export function conflict(
79
+ message?: string,
80
+ details?: Record<string, Value>,
81
+ ): never {
82
+ throwAppError('CONFLICT', message, details)
83
+ }
84
+
85
+ /**
86
+ * Throw INTERNAL error (500).
87
+ * Use for internal server errors and unexpected conditions.
88
+ */
89
+ export function internal(
90
+ message?: string,
91
+ details?: Record<string, Value>,
92
+ ): never {
93
+ throwAppError('INTERNAL', message, details)
94
+ }
95
+
96
+ /**
97
+ * Throw FEATURE_DISABLED error.
98
+ * Use when a feature flag is off.
99
+ */
100
+ export function featureDisabled(
101
+ message?: string,
102
+ details?: Record<string, Value>,
103
+ ): never {
104
+ throwAppError('FEATURE_DISABLED', message, details)
105
+ }
106
+
107
+ /**
108
+ * Throw RATE_LIMITED error (429).
109
+ * Use when rate limits are exceeded.
110
+ */
111
+ export function rateLimited(
112
+ message?: string,
113
+ details?: Record<string, Value>,
114
+ ): never {
115
+ throwAppError('RATE_LIMITED', message, details)
116
+ }
117
+
118
+ /**
119
+ * Throw VALIDATION_ERROR (422).
120
+ * Use for form validation errors with field-level details.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * const errors: ValidationError[] = []
125
+ * if (!email.includes('@')) {
126
+ * errors.push({ field: 'email', message: 'Invalid email address' })
127
+ * }
128
+ * if (errors.length > 0) {
129
+ * validationError(errors, 'Please check your input')
130
+ * }
131
+ * ```
132
+ */
133
+ export function validationError(
134
+ validationErrors: ValidationError[],
135
+ message?: string,
136
+ details?: Record<string, Value>,
137
+ ): never {
138
+ throwAppError('VALIDATION_ERROR', message, details, validationErrors)
139
+ }
140
+
141
+ /**
142
+ * Create error helpers pre-enriched with metadata.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * const err = withErrorMeta({
147
+ * functionName: 'users.update',
148
+ * requestId: 'abc123',
149
+ * })
150
+ * err.notFound('User not found')
151
+ * ```
152
+ */
153
+ export function withErrorMeta(meta: {
154
+ functionName?: string
155
+ requestId?: string
156
+ userId?: string
157
+ }) {
158
+ const mergeDetails = (d?: Record<string, Value>): Record<string, Value> => ({
159
+ ...(d ?? {}),
160
+ ...meta,
161
+ })
162
+
163
+ return {
164
+ unauthenticated: (m?: string, d?: Record<string, Value>) =>
165
+ unauthenticated(m, mergeDetails(d)),
166
+ forbidden: (m?: string, d?: Record<string, Value>) =>
167
+ forbidden(m, mergeDetails(d)),
168
+ notFound: (m?: string, d?: Record<string, Value>) =>
169
+ notFound(m, mergeDetails(d)),
170
+ badRequest: (m?: string, d?: Record<string, Value>) =>
171
+ badRequest(m, mergeDetails(d)),
172
+ conflict: (m?: string, d?: Record<string, Value>) =>
173
+ conflict(m, mergeDetails(d)),
174
+ internal: (m?: string, d?: Record<string, Value>) =>
175
+ internal(m, mergeDetails(d)),
176
+ validationError: (
177
+ ve: ValidationError[],
178
+ m?: string,
179
+ d?: Record<string, Value>,
180
+ ) => validationError(ve, m, mergeDetails(d)),
181
+ rateLimited: (m?: string, d?: Record<string, Value>) =>
182
+ rateLimited(m, mergeDetails(d)),
183
+ }
184
+ }
@@ -0,0 +1,57 @@
1
+ // @geenius-tools/convex-errors — src/types.ts
2
+
3
+ import type { Value } from 'convex/values'
4
+
5
+ /**
6
+ * Standard error codes for Convex applications.
7
+ */
8
+ export type AppErrorCode =
9
+ | 'UNAUTHENTICATED'
10
+ | 'FORBIDDEN'
11
+ | 'NOT_FOUND'
12
+ | 'BAD_REQUEST'
13
+ | 'CONFLICT'
14
+ | 'INTERNAL'
15
+ | 'VALIDATION_ERROR'
16
+ | 'RATE_LIMITED'
17
+ | 'FEATURE_DISABLED'
18
+
19
+ /**
20
+ * A field-level validation error.
21
+ */
22
+ export type ValidationError = {
23
+ field: string
24
+ message: string
25
+ code?: string
26
+ }
27
+
28
+ /**
29
+ * Structured payload thrown via `ConvexError<AppErrorPayload>`.
30
+ */
31
+ export type AppErrorPayload = {
32
+ code: AppErrorCode
33
+ message?: string
34
+ details?: Record<string, Value>
35
+ validationErrors?: ValidationError[]
36
+ _metadata?: {
37
+ timestamp: string
38
+ functionName?: string
39
+ userId?: string
40
+ requestId?: string
41
+ }
42
+ }
43
+
44
+ /**
45
+ * HTTP status code mapping for error codes.
46
+ */
47
+ export const HTTP_STATUS_MAP: Record<AppErrorCode, number> = {
48
+ UNAUTHENTICATED: 401,
49
+ FORBIDDEN: 403,
50
+ NOT_FOUND: 404,
51
+ BAD_REQUEST: 400,
52
+ CONFLICT: 409,
53
+ INTERNAL: 500,
54
+ VALIDATION_ERROR: 422,
55
+ RATE_LIMITED: 429,
56
+ FEATURE_DISABLED: 403,
57
+ }