@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.
- package/.changeset/config.json +11 -0
- package/.env.example +2 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +29 -0
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/.prettierrc +7 -0
- package/.project/ACCOUNT.yaml +4 -0
- package/.project/IDEAS.yaml +7 -0
- package/.project/PROJECT.yaml +11 -0
- package/.project/ROADMAP.yaml +15 -0
- package/CHANGELOG.md +16 -0
- package/CODE_OF_CONDUCT.md +26 -0
- package/CONTRIBUTING.md +69 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/SECURITY.md +18 -0
- package/SUPPORT.md +14 -0
- package/package.json +75 -0
- package/packages/convex/shared/README.md +1 -0
- package/packages/convex/shared/package.json +42 -0
- package/packages/convex/shared/src/audit/index.ts +5 -0
- package/packages/convex/shared/src/audit/presets.ts +165 -0
- package/packages/convex/shared/src/audit/schema.ts +85 -0
- package/packages/convex/shared/src/audit/write.ts +102 -0
- package/packages/convex/shared/src/extract.ts +75 -0
- package/packages/convex/shared/src/index.ts +41 -0
- package/packages/convex/shared/src/messages.ts +45 -0
- package/packages/convex/shared/src/security.ts +112 -0
- package/packages/convex/shared/src/throw.ts +184 -0
- package/packages/convex/shared/src/types.ts +57 -0
- package/packages/convex/shared/src/utils.ts +58 -0
- package/packages/convex/shared/tsconfig.json +28 -0
- package/packages/convex/shared/tsup.config.ts +12 -0
- package/packages/devtools/package.json +27 -0
- package/packages/devtools/react/README.md +1 -0
- package/packages/devtools/react/package.json +53 -0
- package/packages/devtools/react/src/components/DesignPreview.tsx +59 -0
- package/packages/devtools/react/src/components/DesignSwitcherDropdown.tsx +99 -0
- package/packages/devtools/react/src/components/DevSidebar.tsx +247 -0
- package/packages/devtools/react/src/components/DevToolbar.tsx +242 -0
- package/packages/devtools/react/src/components/GitHubIssueDialog.tsx +402 -0
- package/packages/devtools/react/src/components/InspectorOverlay.tsx +312 -0
- package/packages/devtools/react/src/components/PageLoadWaterfall.tsx +144 -0
- package/packages/devtools/react/src/components/PerformancePanel.tsx +330 -0
- package/packages/devtools/react/src/context/DevModeContext.tsx +226 -0
- package/packages/devtools/react/src/context/PerformanceContext.tsx +143 -0
- package/packages/devtools/react/src/data/designs.ts +13 -0
- package/packages/devtools/react/src/hooks/useGitHubLabels.ts +47 -0
- package/packages/devtools/react/src/hooks/useVirtualList.ts +124 -0
- package/packages/devtools/react/src/index.ts +77 -0
- package/packages/devtools/react/src/panels/ConvexSpy.tsx +130 -0
- package/packages/devtools/react/src/panels/DatabaseSeeder.tsx +116 -0
- package/packages/devtools/react/src/panels/DevModePhase2.tsx +191 -0
- package/packages/devtools/react/src/panels/DevModePhase3.tsx +234 -0
- package/packages/devtools/react/src/panels/FeatureFlagsToggle.tsx +104 -0
- package/packages/devtools/react/src/panels/QuickRouteJump.tsx +152 -0
- package/packages/devtools/react/src/services/github-service.ts +247 -0
- package/packages/devtools/react/tsconfig.json +31 -0
- package/packages/devtools/react/tsup.config.ts +18 -0
- package/packages/devtools/solidjs/README.md +1 -0
- package/packages/devtools/solidjs/package.json +49 -0
- package/packages/devtools/solidjs/src/components/DesignPreview.tsx +51 -0
- package/packages/devtools/solidjs/src/components/DesignSwitcherDropdown.tsx +95 -0
- package/packages/devtools/solidjs/src/components/DevSidebar.tsx +247 -0
- package/packages/devtools/solidjs/src/components/DevToolbar.tsx +242 -0
- package/packages/devtools/solidjs/src/components/GitHubIssueDialog.tsx +400 -0
- package/packages/devtools/solidjs/src/components/InspectorOverlay.tsx +311 -0
- package/packages/devtools/solidjs/src/components/PageLoadWaterfall.tsx +144 -0
- package/packages/devtools/solidjs/src/components/PerformancePanel.tsx +330 -0
- package/packages/devtools/solidjs/src/context/DevModeContext.tsx +216 -0
- package/packages/devtools/solidjs/src/context/PerformanceContext.tsx +135 -0
- package/packages/devtools/solidjs/src/data/designs.ts +13 -0
- package/packages/devtools/solidjs/src/hooks/createGitHubLabels.ts +47 -0
- package/packages/devtools/solidjs/src/index.ts +64 -0
- package/packages/devtools/solidjs/src/services/github-service.ts +247 -0
- package/packages/devtools/solidjs/tsconfig.json +21 -0
- package/packages/devtools/src/index.ts +377 -0
- package/packages/devtools/tsup.config.ts +12 -0
- package/packages/env/package.json +30 -0
- package/packages/env/src/index.ts +264 -0
- package/packages/env/tsup.config.ts +12 -0
- package/packages/errors/package.json +27 -0
- package/packages/errors/react/README.md +1 -0
- package/packages/errors/react/package.json +72 -0
- package/packages/errors/react/src/analytics.ts +16 -0
- package/packages/errors/react/src/components/ErrorBoundary.tsx +248 -0
- package/packages/errors/react/src/components/ErrorDisplay.tsx +328 -0
- package/packages/errors/react/src/components/ValidationErrors.tsx +102 -0
- package/packages/errors/react/src/config.ts +199 -0
- package/packages/errors/react/src/constants.ts +74 -0
- package/packages/errors/react/src/hooks/useErrorBoundary.ts +92 -0
- package/packages/errors/react/src/hooks/useErrorHandler.ts +87 -0
- package/packages/errors/react/src/index.ts +96 -0
- package/packages/errors/react/src/types.ts +102 -0
- package/packages/errors/react/src/utils/errorMessages.ts +35 -0
- package/packages/errors/react/src/utils/errorPolicy.ts +139 -0
- package/packages/errors/react/src/utils/extractAppError.ts +174 -0
- package/packages/errors/react/src/utils/formatError.ts +112 -0
- package/packages/errors/react/tsconfig.json +25 -0
- package/packages/errors/react/tsup.config.ts +24 -0
- package/packages/errors/solidjs/README.md +1 -0
- package/packages/errors/solidjs/package.json +46 -0
- package/packages/errors/solidjs/src/components/ErrorDisplay.tsx +179 -0
- package/packages/errors/solidjs/src/config.ts +98 -0
- package/packages/errors/solidjs/src/hooks/createErrorHandler.ts +107 -0
- package/packages/errors/solidjs/src/index.ts +61 -0
- package/packages/errors/solidjs/src/types.ts +34 -0
- package/packages/errors/solidjs/src/utils/errorPolicy.ts +56 -0
- package/packages/errors/solidjs/src/utils/extractAppError.ts +94 -0
- package/packages/errors/solidjs/src/utils/formatError.ts +33 -0
- package/packages/errors/solidjs/tsconfig.json +26 -0
- package/packages/errors/solidjs/tsup.config.ts +21 -0
- package/packages/errors/src/index.ts +320 -0
- package/packages/errors/tsup.config.ts +12 -0
- package/packages/logger/package.json +27 -0
- package/packages/logger/react/README.md +1 -0
- package/packages/logger/react/package.json +46 -0
- package/packages/logger/react/src/index.ts +4 -0
- package/packages/logger/react/src/useMetrics.ts +42 -0
- package/packages/logger/react/src/usePerformanceLog.ts +61 -0
- package/packages/logger/react/tsconfig.json +31 -0
- package/packages/logger/react/tsup.config.ts +12 -0
- package/packages/logger/solidjs/README.md +1 -0
- package/packages/logger/solidjs/package.json +45 -0
- package/packages/logger/solidjs/src/createMetrics.ts +37 -0
- package/packages/logger/solidjs/src/createPerformanceLog.ts +58 -0
- package/packages/logger/solidjs/src/index.ts +4 -0
- package/packages/logger/solidjs/tsconfig.json +32 -0
- package/packages/logger/solidjs/tsup.config.ts +12 -0
- package/packages/logger/src/index.ts +363 -0
- package/packages/logger/tsup.config.ts +12 -0
- package/packages/perf/package.json +27 -0
- package/packages/perf/react/README.md +1 -0
- package/packages/perf/react/package.json +59 -0
- package/packages/perf/react/src/components/PerformanceDashboard.tsx +257 -0
- package/packages/perf/react/src/hooks/useMonitoredQuery.ts +89 -0
- package/packages/perf/react/src/hooks/usePerformanceMetrics.ts +78 -0
- package/packages/perf/react/src/index.ts +33 -0
- package/packages/perf/react/src/services/PerformanceMonitor.ts +313 -0
- package/packages/perf/react/src/types.ts +77 -0
- package/packages/perf/react/tsconfig.json +25 -0
- package/packages/perf/react/tsup.config.ts +19 -0
- package/packages/perf/solidjs/README.md +1 -0
- package/packages/perf/solidjs/package.json +41 -0
- package/packages/perf/solidjs/src/components/PerformanceDashboard.tsx +207 -0
- package/packages/perf/solidjs/src/hooks/createPerformanceMetrics.ts +73 -0
- package/packages/perf/solidjs/src/index.ts +31 -0
- package/packages/perf/solidjs/src/services/PerformanceMonitor.ts +134 -0
- package/packages/perf/solidjs/src/types.ts +78 -0
- package/packages/perf/solidjs/tsconfig.json +26 -0
- package/packages/perf/solidjs/tsup.config.ts +14 -0
- package/packages/perf/src/index.ts +410 -0
- package/packages/perf/tsup.config.ts +12 -0
- 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
|
+
}
|