secure-vibe-runtime 0.0.0-stage → 7.3.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.
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Schema fragments and validators for the security log.
3
+ *
4
+ * Apps spread `securityTables` into their own defineSchema() so the three
5
+ * tables, their indexes and their validators are owned by this package: an
6
+ * index or field change ships as a package update followed by the app's
7
+ * normal `npx convex deploy`, not as a hand-edited schema in every app.
8
+ *
9
+ * `securityEvents.userId` references a `users` table the app defines; Convex
10
+ * validates that reference when the app's schema is pushed.
11
+ */
12
+ import { DataModelFromSchemaDefinition, GenericMutationCtx, GenericQueryCtx, DocumentByName } from "convex/server";
13
+ import { Infer } from "convex/values";
14
+ export declare const eventTypeValidator: import("convex/values").VUnion<"csrf_validation_failed" | "rate_limit_exceeded" | "origin_mismatch" | "invalid_api_key" | "fingerprint_change" | "suspicious_activity" | "jwt_validation_failed" | "unauthorized_access" | "input_validation_failed" | "replay_detected" | "not_found_enumeration" | "jwt_algorithm_attack" | "tenant_isolation_attack" | "jwt_replay_attack" | "xss_attempt" | "fingerprint_manipulation" | "http_origin_blocked" | "prompt_injection_attempt" | "ai_response_validation_failed", [import("convex/values").VLiteral<"origin_mismatch", "required">, import("convex/values").VLiteral<"rate_limit_exceeded", "required">, import("convex/values").VLiteral<"invalid_api_key", "required">, import("convex/values").VLiteral<"fingerprint_change", "required">, import("convex/values").VLiteral<"suspicious_activity", "required">, import("convex/values").VLiteral<"jwt_validation_failed", "required">, import("convex/values").VLiteral<"unauthorized_access", "required">, import("convex/values").VLiteral<"input_validation_failed", "required">, import("convex/values").VLiteral<"replay_detected", "required">, import("convex/values").VLiteral<"not_found_enumeration", "required">, import("convex/values").VLiteral<"jwt_algorithm_attack", "required">, import("convex/values").VLiteral<"tenant_isolation_attack", "required">, import("convex/values").VLiteral<"jwt_replay_attack", "required">, import("convex/values").VLiteral<"xss_attempt", "required">, import("convex/values").VLiteral<"fingerprint_manipulation", "required">, import("convex/values").VLiteral<"http_origin_blocked", "required">, import("convex/values").VLiteral<"prompt_injection_attempt", "required">, import("convex/values").VLiteral<"ai_response_validation_failed", "required">, import("convex/values").VLiteral<"csrf_validation_failed", "required">], "required", never>;
15
+ export declare const severityValidator: import("convex/values").VUnion<"critical" | "medium" | "low" | "high", [import("convex/values").VLiteral<"low", "required">, import("convex/values").VLiteral<"medium", "required">, import("convex/values").VLiteral<"high", "required">, import("convex/values").VLiteral<"critical", "required">], "required", never>;
16
+ export declare const metadataValidator: import("convex/values").VObject<{
17
+ origin?: string | undefined;
18
+ endpoint?: string | undefined;
19
+ ipAddress?: string | undefined;
20
+ errorMessage?: string | undefined;
21
+ fingerprint?: string | undefined;
22
+ endUserEmail?: string | undefined;
23
+ endUserName?: string | undefined;
24
+ endUserId?: string | undefined;
25
+ actionType?: string | undefined;
26
+ requestPayload?: string | undefined;
27
+ }, {
28
+ origin: import("convex/values").VString<string | undefined, "optional">;
29
+ ipAddress: import("convex/values").VString<string | undefined, "optional">;
30
+ fingerprint: import("convex/values").VString<string | undefined, "optional">;
31
+ endpoint: import("convex/values").VString<string | undefined, "optional">;
32
+ errorMessage: import("convex/values").VString<string | undefined, "optional">;
33
+ endUserEmail: import("convex/values").VString<string | undefined, "optional">;
34
+ endUserName: import("convex/values").VString<string | undefined, "optional">;
35
+ endUserId: import("convex/values").VString<string | undefined, "optional">;
36
+ actionType: import("convex/values").VString<string | undefined, "optional">;
37
+ requestPayload: import("convex/values").VString<string | undefined, "optional">;
38
+ }, "required", "origin" | "endpoint" | "ipAddress" | "errorMessage" | "fingerprint" | "endUserEmail" | "endUserName" | "endUserId" | "actionType" | "requestPayload">;
39
+ export type SecurityEventType = Infer<typeof eventTypeValidator>;
40
+ export type SecuritySeverity = Infer<typeof severityValidator>;
41
+ export type SecurityMetadata = Infer<typeof metadataValidator>;
42
+ export declare const securityTables: {
43
+ securityEvents: import("convex/server").TableDefinition<import("convex/values").VObject<{
44
+ userId?: import("convex/values").GenericId<"users"> | undefined;
45
+ eventType: "csrf_validation_failed" | "rate_limit_exceeded" | "origin_mismatch" | "invalid_api_key" | "fingerprint_change" | "suspicious_activity" | "jwt_validation_failed" | "unauthorized_access" | "input_validation_failed" | "replay_detected" | "not_found_enumeration" | "jwt_algorithm_attack" | "tenant_isolation_attack" | "jwt_replay_attack" | "xss_attempt" | "fingerprint_manipulation" | "http_origin_blocked" | "prompt_injection_attempt" | "ai_response_validation_failed";
46
+ severity: "critical" | "medium" | "low" | "high";
47
+ metadata: {
48
+ origin?: string | undefined;
49
+ endpoint?: string | undefined;
50
+ ipAddress?: string | undefined;
51
+ errorMessage?: string | undefined;
52
+ fingerprint?: string | undefined;
53
+ endUserEmail?: string | undefined;
54
+ endUserName?: string | undefined;
55
+ endUserId?: string | undefined;
56
+ actionType?: string | undefined;
57
+ requestPayload?: string | undefined;
58
+ };
59
+ timestamp: number;
60
+ isRead: boolean;
61
+ }, {
62
+ userId: import("convex/values").VId<import("convex/values").GenericId<"users"> | undefined, "optional">;
63
+ eventType: import("convex/values").VUnion<"csrf_validation_failed" | "rate_limit_exceeded" | "origin_mismatch" | "invalid_api_key" | "fingerprint_change" | "suspicious_activity" | "jwt_validation_failed" | "unauthorized_access" | "input_validation_failed" | "replay_detected" | "not_found_enumeration" | "jwt_algorithm_attack" | "tenant_isolation_attack" | "jwt_replay_attack" | "xss_attempt" | "fingerprint_manipulation" | "http_origin_blocked" | "prompt_injection_attempt" | "ai_response_validation_failed", [import("convex/values").VLiteral<"origin_mismatch", "required">, import("convex/values").VLiteral<"rate_limit_exceeded", "required">, import("convex/values").VLiteral<"invalid_api_key", "required">, import("convex/values").VLiteral<"fingerprint_change", "required">, import("convex/values").VLiteral<"suspicious_activity", "required">, import("convex/values").VLiteral<"jwt_validation_failed", "required">, import("convex/values").VLiteral<"unauthorized_access", "required">, import("convex/values").VLiteral<"input_validation_failed", "required">, import("convex/values").VLiteral<"replay_detected", "required">, import("convex/values").VLiteral<"not_found_enumeration", "required">, import("convex/values").VLiteral<"jwt_algorithm_attack", "required">, import("convex/values").VLiteral<"tenant_isolation_attack", "required">, import("convex/values").VLiteral<"jwt_replay_attack", "required">, import("convex/values").VLiteral<"xss_attempt", "required">, import("convex/values").VLiteral<"fingerprint_manipulation", "required">, import("convex/values").VLiteral<"http_origin_blocked", "required">, import("convex/values").VLiteral<"prompt_injection_attempt", "required">, import("convex/values").VLiteral<"ai_response_validation_failed", "required">, import("convex/values").VLiteral<"csrf_validation_failed", "required">], "required", never>;
64
+ severity: import("convex/values").VUnion<"critical" | "medium" | "low" | "high", [import("convex/values").VLiteral<"low", "required">, import("convex/values").VLiteral<"medium", "required">, import("convex/values").VLiteral<"high", "required">, import("convex/values").VLiteral<"critical", "required">], "required", never>;
65
+ metadata: import("convex/values").VObject<{
66
+ origin?: string | undefined;
67
+ endpoint?: string | undefined;
68
+ ipAddress?: string | undefined;
69
+ errorMessage?: string | undefined;
70
+ fingerprint?: string | undefined;
71
+ endUserEmail?: string | undefined;
72
+ endUserName?: string | undefined;
73
+ endUserId?: string | undefined;
74
+ actionType?: string | undefined;
75
+ requestPayload?: string | undefined;
76
+ }, {
77
+ origin: import("convex/values").VString<string | undefined, "optional">;
78
+ ipAddress: import("convex/values").VString<string | undefined, "optional">;
79
+ fingerprint: import("convex/values").VString<string | undefined, "optional">;
80
+ endpoint: import("convex/values").VString<string | undefined, "optional">;
81
+ errorMessage: import("convex/values").VString<string | undefined, "optional">;
82
+ endUserEmail: import("convex/values").VString<string | undefined, "optional">;
83
+ endUserName: import("convex/values").VString<string | undefined, "optional">;
84
+ endUserId: import("convex/values").VString<string | undefined, "optional">;
85
+ actionType: import("convex/values").VString<string | undefined, "optional">;
86
+ requestPayload: import("convex/values").VString<string | undefined, "optional">;
87
+ }, "required", "origin" | "endpoint" | "ipAddress" | "errorMessage" | "fingerprint" | "endUserEmail" | "endUserName" | "endUserId" | "actionType" | "requestPayload">;
88
+ timestamp: import("convex/values").VFloat64<number, "required">;
89
+ isRead: import("convex/values").VBoolean<boolean, "required">;
90
+ }, "required", "eventType" | "severity" | "metadata" | "userId" | "timestamp" | "isRead" | "metadata.origin" | "metadata.endpoint" | "metadata.ipAddress" | "metadata.errorMessage" | "metadata.fingerprint" | "metadata.endUserEmail" | "metadata.endUserName" | "metadata.endUserId" | "metadata.actionType" | "metadata.requestPayload">, {
91
+ byUser: ["userId", "timestamp", "_creationTime"];
92
+ bySeverity: ["userId", "severity", "timestamp", "_creationTime"];
93
+ byUnread: ["userId", "isRead", "timestamp", "_creationTime"];
94
+ byTimestamp: ["timestamp", "_creationTime"];
95
+ byTypeTime: ["eventType", "timestamp", "_creationTime"];
96
+ bySeverityTime: ["severity", "timestamp", "_creationTime"];
97
+ byReadTime: ["isRead", "timestamp", "_creationTime"];
98
+ byTypeSeverityTime: ["eventType", "severity", "timestamp", "_creationTime"];
99
+ byTypeReadTime: ["eventType", "isRead", "timestamp", "_creationTime"];
100
+ bySeverityReadTime: ["severity", "isRead", "timestamp", "_creationTime"];
101
+ byTypeSeverityReadTime: ["eventType", "severity", "isRead", "timestamp", "_creationTime"];
102
+ }, {}, {}>;
103
+ securityEventCounts: import("convex/server").TableDefinition<import("convex/values").VObject<{
104
+ hour: number;
105
+ counts: Record<string, number>;
106
+ unread: Record<string, number>;
107
+ }, {
108
+ hour: import("convex/values").VFloat64<number, "required">;
109
+ counts: import("convex/values").VRecord<Record<string, number>, import("convex/values").VString<string, "required">, import("convex/values").VFloat64<number, "required">, "required", string>;
110
+ unread: import("convex/values").VRecord<Record<string, number>, import("convex/values").VString<string, "required">, import("convex/values").VFloat64<number, "required">, "required", string>;
111
+ }, "required", "hour" | "counts" | "unread" | `counts.${string}` | `unread.${string}`>, {
112
+ byHour: ["hour", "_creationTime"];
113
+ }, {}, {}>;
114
+ securityCountsRebuild: import("convex/server").TableDefinition<import("convex/values").VObject<{
115
+ runId: string;
116
+ upTo: number;
117
+ startedAt: number;
118
+ }, {
119
+ runId: import("convex/values").VString<string, "required">;
120
+ upTo: import("convex/values").VFloat64<number, "required">;
121
+ startedAt: import("convex/values").VFloat64<number, "required">;
122
+ }, "required", "runId" | "upTo" | "startedAt">, {}, {}, {}>;
123
+ };
124
+ /** The three tables as a standalone schema; apps spread `securityTables` instead. */
125
+ export declare const securitySchema: import("convex/server").SchemaDefinition<{
126
+ securityEvents: import("convex/server").TableDefinition<import("convex/values").VObject<{
127
+ userId?: import("convex/values").GenericId<"users"> | undefined;
128
+ eventType: "csrf_validation_failed" | "rate_limit_exceeded" | "origin_mismatch" | "invalid_api_key" | "fingerprint_change" | "suspicious_activity" | "jwt_validation_failed" | "unauthorized_access" | "input_validation_failed" | "replay_detected" | "not_found_enumeration" | "jwt_algorithm_attack" | "tenant_isolation_attack" | "jwt_replay_attack" | "xss_attempt" | "fingerprint_manipulation" | "http_origin_blocked" | "prompt_injection_attempt" | "ai_response_validation_failed";
129
+ severity: "critical" | "medium" | "low" | "high";
130
+ metadata: {
131
+ origin?: string | undefined;
132
+ endpoint?: string | undefined;
133
+ ipAddress?: string | undefined;
134
+ errorMessage?: string | undefined;
135
+ fingerprint?: string | undefined;
136
+ endUserEmail?: string | undefined;
137
+ endUserName?: string | undefined;
138
+ endUserId?: string | undefined;
139
+ actionType?: string | undefined;
140
+ requestPayload?: string | undefined;
141
+ };
142
+ timestamp: number;
143
+ isRead: boolean;
144
+ }, {
145
+ userId: import("convex/values").VId<import("convex/values").GenericId<"users"> | undefined, "optional">;
146
+ eventType: import("convex/values").VUnion<"csrf_validation_failed" | "rate_limit_exceeded" | "origin_mismatch" | "invalid_api_key" | "fingerprint_change" | "suspicious_activity" | "jwt_validation_failed" | "unauthorized_access" | "input_validation_failed" | "replay_detected" | "not_found_enumeration" | "jwt_algorithm_attack" | "tenant_isolation_attack" | "jwt_replay_attack" | "xss_attempt" | "fingerprint_manipulation" | "http_origin_blocked" | "prompt_injection_attempt" | "ai_response_validation_failed", [import("convex/values").VLiteral<"origin_mismatch", "required">, import("convex/values").VLiteral<"rate_limit_exceeded", "required">, import("convex/values").VLiteral<"invalid_api_key", "required">, import("convex/values").VLiteral<"fingerprint_change", "required">, import("convex/values").VLiteral<"suspicious_activity", "required">, import("convex/values").VLiteral<"jwt_validation_failed", "required">, import("convex/values").VLiteral<"unauthorized_access", "required">, import("convex/values").VLiteral<"input_validation_failed", "required">, import("convex/values").VLiteral<"replay_detected", "required">, import("convex/values").VLiteral<"not_found_enumeration", "required">, import("convex/values").VLiteral<"jwt_algorithm_attack", "required">, import("convex/values").VLiteral<"tenant_isolation_attack", "required">, import("convex/values").VLiteral<"jwt_replay_attack", "required">, import("convex/values").VLiteral<"xss_attempt", "required">, import("convex/values").VLiteral<"fingerprint_manipulation", "required">, import("convex/values").VLiteral<"http_origin_blocked", "required">, import("convex/values").VLiteral<"prompt_injection_attempt", "required">, import("convex/values").VLiteral<"ai_response_validation_failed", "required">, import("convex/values").VLiteral<"csrf_validation_failed", "required">], "required", never>;
147
+ severity: import("convex/values").VUnion<"critical" | "medium" | "low" | "high", [import("convex/values").VLiteral<"low", "required">, import("convex/values").VLiteral<"medium", "required">, import("convex/values").VLiteral<"high", "required">, import("convex/values").VLiteral<"critical", "required">], "required", never>;
148
+ metadata: import("convex/values").VObject<{
149
+ origin?: string | undefined;
150
+ endpoint?: string | undefined;
151
+ ipAddress?: string | undefined;
152
+ errorMessage?: string | undefined;
153
+ fingerprint?: string | undefined;
154
+ endUserEmail?: string | undefined;
155
+ endUserName?: string | undefined;
156
+ endUserId?: string | undefined;
157
+ actionType?: string | undefined;
158
+ requestPayload?: string | undefined;
159
+ }, {
160
+ origin: import("convex/values").VString<string | undefined, "optional">;
161
+ ipAddress: import("convex/values").VString<string | undefined, "optional">;
162
+ fingerprint: import("convex/values").VString<string | undefined, "optional">;
163
+ endpoint: import("convex/values").VString<string | undefined, "optional">;
164
+ errorMessage: import("convex/values").VString<string | undefined, "optional">;
165
+ endUserEmail: import("convex/values").VString<string | undefined, "optional">;
166
+ endUserName: import("convex/values").VString<string | undefined, "optional">;
167
+ endUserId: import("convex/values").VString<string | undefined, "optional">;
168
+ actionType: import("convex/values").VString<string | undefined, "optional">;
169
+ requestPayload: import("convex/values").VString<string | undefined, "optional">;
170
+ }, "required", "origin" | "endpoint" | "ipAddress" | "errorMessage" | "fingerprint" | "endUserEmail" | "endUserName" | "endUserId" | "actionType" | "requestPayload">;
171
+ timestamp: import("convex/values").VFloat64<number, "required">;
172
+ isRead: import("convex/values").VBoolean<boolean, "required">;
173
+ }, "required", "eventType" | "severity" | "metadata" | "userId" | "timestamp" | "isRead" | "metadata.origin" | "metadata.endpoint" | "metadata.ipAddress" | "metadata.errorMessage" | "metadata.fingerprint" | "metadata.endUserEmail" | "metadata.endUserName" | "metadata.endUserId" | "metadata.actionType" | "metadata.requestPayload">, {
174
+ byUser: ["userId", "timestamp", "_creationTime"];
175
+ bySeverity: ["userId", "severity", "timestamp", "_creationTime"];
176
+ byUnread: ["userId", "isRead", "timestamp", "_creationTime"];
177
+ byTimestamp: ["timestamp", "_creationTime"];
178
+ byTypeTime: ["eventType", "timestamp", "_creationTime"];
179
+ bySeverityTime: ["severity", "timestamp", "_creationTime"];
180
+ byReadTime: ["isRead", "timestamp", "_creationTime"];
181
+ byTypeSeverityTime: ["eventType", "severity", "timestamp", "_creationTime"];
182
+ byTypeReadTime: ["eventType", "isRead", "timestamp", "_creationTime"];
183
+ bySeverityReadTime: ["severity", "isRead", "timestamp", "_creationTime"];
184
+ byTypeSeverityReadTime: ["eventType", "severity", "isRead", "timestamp", "_creationTime"];
185
+ }, {}, {}>;
186
+ securityEventCounts: import("convex/server").TableDefinition<import("convex/values").VObject<{
187
+ hour: number;
188
+ counts: Record<string, number>;
189
+ unread: Record<string, number>;
190
+ }, {
191
+ hour: import("convex/values").VFloat64<number, "required">;
192
+ counts: import("convex/values").VRecord<Record<string, number>, import("convex/values").VString<string, "required">, import("convex/values").VFloat64<number, "required">, "required", string>;
193
+ unread: import("convex/values").VRecord<Record<string, number>, import("convex/values").VString<string, "required">, import("convex/values").VFloat64<number, "required">, "required", string>;
194
+ }, "required", "hour" | "counts" | "unread" | `counts.${string}` | `unread.${string}`>, {
195
+ byHour: ["hour", "_creationTime"];
196
+ }, {}, {}>;
197
+ securityCountsRebuild: import("convex/server").TableDefinition<import("convex/values").VObject<{
198
+ runId: string;
199
+ upTo: number;
200
+ startedAt: number;
201
+ }, {
202
+ runId: import("convex/values").VString<string, "required">;
203
+ upTo: import("convex/values").VFloat64<number, "required">;
204
+ startedAt: import("convex/values").VFloat64<number, "required">;
205
+ }, "required", "runId" | "upTo" | "startedAt">, {}, {}, {}>;
206
+ }, true>;
207
+ export type SecurityDataModel = DataModelFromSchemaDefinition<typeof securitySchema>;
208
+ /**
209
+ * An app's MutationCtx/QueryCtx is assignable to these because its data model
210
+ * is a superset of SecurityDataModel (verified with tsc; no cast needed).
211
+ */
212
+ export type SecurityMutationCtx = GenericMutationCtx<SecurityDataModel>;
213
+ export type SecurityQueryCtx = GenericQueryCtx<SecurityDataModel>;
214
+ export type SecurityEventDoc = DocumentByName<SecurityDataModel, "securityEvents">;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Schema fragments and validators for the security log.
3
+ *
4
+ * Apps spread `securityTables` into their own defineSchema() so the three
5
+ * tables, their indexes and their validators are owned by this package: an
6
+ * index or field change ships as a package update followed by the app's
7
+ * normal `npx convex deploy`, not as a hand-edited schema in every app.
8
+ *
9
+ * `securityEvents.userId` references a `users` table the app defines; Convex
10
+ * validates that reference when the app's schema is pushed.
11
+ */
12
+ import { defineSchema, defineTable, } from "convex/server";
13
+ import { v } from "convex/values";
14
+ export const eventTypeValidator = v.union(v.literal("origin_mismatch"), v.literal("rate_limit_exceeded"), v.literal("invalid_api_key"), v.literal("fingerprint_change"), v.literal("suspicious_activity"), v.literal("jwt_validation_failed"), v.literal("unauthorized_access"), v.literal("input_validation_failed"), v.literal("replay_detected"), v.literal("not_found_enumeration"), v.literal("jwt_algorithm_attack"), v.literal("tenant_isolation_attack"), v.literal("jwt_replay_attack"), v.literal("xss_attempt"), v.literal("fingerprint_manipulation"), v.literal("http_origin_blocked"), v.literal("prompt_injection_attempt"), v.literal("ai_response_validation_failed"), v.literal("csrf_validation_failed"));
15
+ export const severityValidator = v.union(v.literal("low"), v.literal("medium"), v.literal("high"), v.literal("critical"));
16
+ export const metadataValidator = v.object({
17
+ origin: v.optional(v.string()),
18
+ ipAddress: v.optional(v.string()),
19
+ fingerprint: v.optional(v.string()),
20
+ endpoint: v.optional(v.string()),
21
+ errorMessage: v.optional(v.string()),
22
+ endUserEmail: v.optional(v.string()),
23
+ endUserName: v.optional(v.string()),
24
+ endUserId: v.optional(v.string()),
25
+ actionType: v.optional(v.string()),
26
+ requestPayload: v.optional(v.string()),
27
+ });
28
+ export const securityTables = {
29
+ securityEvents: defineTable({
30
+ userId: v.optional(v.id("users")),
31
+ eventType: eventTypeValidator,
32
+ severity: severityValidator,
33
+ metadata: metadataValidator,
34
+ timestamp: v.number(),
35
+ isRead: v.boolean(),
36
+ })
37
+ .index("byUser", ["userId", "timestamp"])
38
+ .index("bySeverity", ["userId", "severity", "timestamp"])
39
+ .index("byUnread", ["userId", "isRead", "timestamp"])
40
+ // Cross-user, time-ordered access. The three indexes above all lead with
41
+ // userId, so none of them can serve the admin dashboard (all users) or the
42
+ // retention cron (all rows older than a cutoff) without a table scan.
43
+ .index("byTimestamp", ["timestamp"])
44
+ // One index per filter combination the admin feed offers (event type,
45
+ // severity, open/closed), each ending in timestamp, so every filtered
46
+ // view is a single index range read one page at a time. Nothing is
47
+ // filtered in JavaScript, so no over-read window is needed.
48
+ .index("byTypeTime", ["eventType", "timestamp"])
49
+ .index("bySeverityTime", ["severity", "timestamp"])
50
+ .index("byReadTime", ["isRead", "timestamp"])
51
+ .index("byTypeSeverityTime", ["eventType", "severity", "timestamp"])
52
+ .index("byTypeReadTime", ["eventType", "isRead", "timestamp"])
53
+ .index("bySeverityReadTime", ["severity", "isRead", "timestamp"])
54
+ .index("byTypeSeverityReadTime", ["eventType", "severity", "isRead", "timestamp"]),
55
+ securityEventCounts: defineTable({
56
+ hour: v.number(),
57
+ counts: v.record(v.string(), v.number()),
58
+ unread: v.record(v.string(), v.number()),
59
+ }).index("byHour", ["hour"]),
60
+ securityCountsRebuild: defineTable({
61
+ runId: v.string(),
62
+ upTo: v.number(),
63
+ startedAt: v.number(),
64
+ }),
65
+ };
66
+ /** The three tables as a standalone schema; apps spread `securityTables` instead. */
67
+ export const securitySchema = defineSchema(securityTables);
package/dist/csrf.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Generates a CSRF token bound to a session ID using HMAC-SHA256
3
+ * @param sessionId - The session ID to bind the token to
4
+ * @returns The generated CSRF token (HMAC digest)
5
+ */
6
+ export declare function generateCsrfToken(sessionId: string): Promise<string>;
7
+ /**
8
+ * Validates a CSRF token against the stored token
9
+ * @param sentToken - Token received from the client
10
+ * @param storedToken - Token stored in the cookie
11
+ * @returns True if tokens match, false otherwise
12
+ */
13
+ export declare function validateCsrfToken(sentToken: string, storedToken: string): boolean;
14
+ /**
15
+ * Generates a UUID for session identification
16
+ * @returns A v4 UUID string
17
+ */
18
+ export declare function generateUUID(): string;
package/dist/csrf.js ADDED
@@ -0,0 +1,66 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import CryptoJS from 'crypto-js';
3
+ /**
4
+ * Reads CSRF_SECRET, or throws.
5
+ *
6
+ * There is deliberately no fallback value. A previous version used
7
+ * `process.env.CSRF_SECRET || 'default-secret'`, which meant a deployment that
8
+ * forgot to set the variable still produced tokens — signed with a constant
9
+ * that is public in this repository, and therefore forgeable by anyone. The
10
+ * failure was invisible: tokens validated, the flow worked, and CSRF protection
11
+ * was simply absent.
12
+ *
13
+ * Failing loudly at first use is the correct trade. `/install` and
14
+ * `/deploy-to-*` both provision CSRF_SECRET, so reaching this throw means the
15
+ * environment is genuinely misconfigured.
16
+ */
17
+ function csrfSecret() {
18
+ const secret = process.env.CSRF_SECRET;
19
+ if (!secret || secret.length < 32) {
20
+ throw new Error('CSRF_SECRET is not set (or is shorter than 32 chars). Refusing to sign CSRF ' +
21
+ 'tokens with a weak or absent key — that would silently disable CSRF protection. ' +
22
+ 'Set CSRF_SECRET in Doppler (or .env.local in legacy mode) and redeploy.');
23
+ }
24
+ return secret;
25
+ }
26
+ /**
27
+ * Generates a CSRF token bound to a session ID using HMAC-SHA256
28
+ * @param sessionId - The session ID to bind the token to
29
+ * @returns The generated CSRF token (HMAC digest)
30
+ */
31
+ export async function generateCsrfToken(sessionId) {
32
+ // Read the secret OUTSIDE the try. A missing secret is a configuration fault
33
+ // that must propagate, not get swallowed and rethrown as a generic failure.
34
+ const secret = csrfSecret();
35
+ try {
36
+ const token = CryptoJS.lib.WordArray.random(32).toString(CryptoJS.enc.Hex);
37
+ const hmac = CryptoJS.HmacSHA256(token + sessionId, secret).toString(CryptoJS.enc.Hex);
38
+ return hmac;
39
+ }
40
+ catch (err) {
41
+ console.error('CSRF token generation error:', err.message);
42
+ throw new Error('Failed to generate CSRF token');
43
+ }
44
+ }
45
+ /**
46
+ * Validates a CSRF token against the stored token
47
+ * @param sentToken - Token received from the client
48
+ * @param storedToken - Token stored in the cookie
49
+ * @returns True if tokens match, false otherwise
50
+ */
51
+ export function validateCsrfToken(sentToken, storedToken) {
52
+ try {
53
+ return sentToken === storedToken;
54
+ }
55
+ catch (err) {
56
+ console.error('CSRF validation error:', err.message);
57
+ return false;
58
+ }
59
+ }
60
+ /**
61
+ * Generates a UUID for session identification
62
+ * @returns A v4 UUID string
63
+ */
64
+ export function generateUUID() {
65
+ return uuidv4();
66
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./csrf";
2
+ export * from "./secrets";
3
+ export * from "./withCsrf";
4
+ export * from "./withRateLimit";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./csrf";
2
+ export * from "./secrets";
3
+ export * from "./withCsrf";
4
+ export * from "./withRateLimit";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Runtime secrets fetcher for Vercel + Doppler.
3
+ *
4
+ * On cold start, calls Doppler's API with a service token to fetch all secrets
5
+ * for the deployment's config and writes them into `process.env` so the rest of
6
+ * the app (Clerk SDK, lib/csrf.ts, etc.) reads them transparently.
7
+ *
8
+ * Local dev path: when DOPPLER_TOKEN is unset (developers run via
9
+ * `doppler run -- next dev`), this is a no-op — values are already in
10
+ * process.env via the doppler CLI wrapper.
11
+ *
12
+ * Cache: a single in-flight Promise is memoized in module scope. Vercel
13
+ * Fluid Compute reuses function instances across requests, so the fetch
14
+ * cost (~100–300ms) is paid once per instance lifetime, not per request.
15
+ *
16
+ * Cache invalidation: app/api/revalidate-secrets calls revalidateSecrets()
17
+ * to drop the cache and force a refetch — used by /rotate after a key change.
18
+ */
19
+ /**
20
+ * Ensure secrets are loaded into process.env before the caller proceeds.
21
+ * Safe to call from many places — fetch happens at most once per instance.
22
+ */
23
+ export declare function ensureSecretsLoaded(): Promise<void>;
24
+ /**
25
+ * Drop the in-memory cache and force a refetch on the next call.
26
+ * Used by /api/revalidate-secrets after rotation.
27
+ */
28
+ export declare function revalidateSecrets(): void;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Runtime secrets fetcher for Vercel + Doppler.
3
+ *
4
+ * On cold start, calls Doppler's API with a service token to fetch all secrets
5
+ * for the deployment's config and writes them into `process.env` so the rest of
6
+ * the app (Clerk SDK, lib/csrf.ts, etc.) reads them transparently.
7
+ *
8
+ * Local dev path: when DOPPLER_TOKEN is unset (developers run via
9
+ * `doppler run -- next dev`), this is a no-op — values are already in
10
+ * process.env via the doppler CLI wrapper.
11
+ *
12
+ * Cache: a single in-flight Promise is memoized in module scope. Vercel
13
+ * Fluid Compute reuses function instances across requests, so the fetch
14
+ * cost (~100–300ms) is paid once per instance lifetime, not per request.
15
+ *
16
+ * Cache invalidation: app/api/revalidate-secrets calls revalidateSecrets()
17
+ * to drop the cache and force a refetch — used by /rotate after a key change.
18
+ */
19
+ const DOPPLER_API_URL = 'https://api.doppler.com/v3/configs/config/secrets/download?format=json';
20
+ // Retry tuning: transient network blips between Vercel egress and Doppler's
21
+ // edge last seconds, not milliseconds. Backoff totals ~3.75s across attempts
22
+ // (250/500/1000/2000ms) so a brief blip becomes a slower cold start, not a 500.
23
+ const MAX_ATTEMPTS = 5;
24
+ const BASE_BACKOFF_MS = 250;
25
+ const PER_ATTEMPT_TIMEOUT_MS = 5_000;
26
+ let secretsPromise = null;
27
+ /**
28
+ * Flatten an error for logging. Undici wraps the real network failure
29
+ * (DNS, TLS, connection reset) in `err.cause`, so a bare `err.message`
30
+ * is often just "fetch failed" — walk the cause chain to keep the detail.
31
+ */
32
+ function describeError(err) {
33
+ const parts = [];
34
+ let current = err;
35
+ while (current instanceof Error) {
36
+ const code = current.code;
37
+ parts.push(`${current.name}: ${current.message}${code ? ` (${code})` : ''}`);
38
+ current = current.cause;
39
+ }
40
+ if (current !== undefined && current !== null)
41
+ parts.push(String(current));
42
+ return parts.join(' <- ') || String(err);
43
+ }
44
+ async function fetchFromDoppler(token) {
45
+ let lastError;
46
+ const started = Date.now();
47
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
48
+ const attemptStarted = Date.now();
49
+ try {
50
+ const res = await fetch(DOPPLER_API_URL, {
51
+ method: 'GET',
52
+ headers: {
53
+ Authorization: `Bearer ${token}`,
54
+ Accept: 'application/json',
55
+ },
56
+ signal: AbortSignal.timeout(PER_ATTEMPT_TIMEOUT_MS),
57
+ });
58
+ if (!res.ok) {
59
+ throw new Error(`Doppler API returned ${res.status} ${res.statusText}`);
60
+ }
61
+ const json = (await res.json());
62
+ if (attempt > 1) {
63
+ console.warn(`[secrets] Doppler fetch recovered on attempt ${attempt}/${MAX_ATTEMPTS} after ${Date.now() - started}ms`);
64
+ }
65
+ return json;
66
+ }
67
+ catch (err) {
68
+ lastError = err;
69
+ console.warn(`[secrets] Doppler fetch attempt ${attempt}/${MAX_ATTEMPTS} failed after ${Date.now() - attemptStarted}ms: ${describeError(err)}`);
70
+ if (attempt < MAX_ATTEMPTS) {
71
+ await new Promise((r) => setTimeout(r, BASE_BACKOFF_MS * 2 ** (attempt - 1)));
72
+ }
73
+ }
74
+ }
75
+ console.error(`[secrets] Doppler fetch exhausted ${MAX_ATTEMPTS} attempts over ${Date.now() - started}ms; last error: ${describeError(lastError)}`);
76
+ throw new Error(`Doppler fetch failed after ${MAX_ATTEMPTS} attempts: ${describeError(lastError)}`);
77
+ }
78
+ async function applySecrets() {
79
+ const token = process.env.DOPPLER_TOKEN;
80
+ if (!token) {
81
+ // Local dev or legacy mode — process.env already populated upstream.
82
+ return;
83
+ }
84
+ const secrets = await fetchFromDoppler(token);
85
+ for (const [key, value] of Object.entries(secrets)) {
86
+ if (typeof value === 'string') {
87
+ process.env[key] = value;
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * Ensure secrets are loaded into process.env before the caller proceeds.
93
+ * Safe to call from many places — fetch happens at most once per instance.
94
+ */
95
+ export function ensureSecretsLoaded() {
96
+ if (!secretsPromise) {
97
+ secretsPromise = applySecrets().catch((err) => {
98
+ // Reset on failure so the next call retries instead of caching the error.
99
+ secretsPromise = null;
100
+ throw err;
101
+ });
102
+ }
103
+ return secretsPromise;
104
+ }
105
+ /**
106
+ * Drop the in-memory cache and force a refetch on the next call.
107
+ * Used by /api/revalidate-secrets after rotation.
108
+ */
109
+ export function revalidateSecrets() {
110
+ secretsPromise = null;
111
+ }
@@ -0,0 +1,22 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ /**
3
+ * Higher-order function that wraps API route handlers with CSRF protection
4
+ * Validates the CSRF token from the request header against the stored cookie
5
+ * Tokens are single-use and cleared after validation
6
+ *
7
+ * @param handler - The API route handler to wrap
8
+ * @returns A wrapped handler with CSRF protection
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { withCsrf } from '@/lib/withCsrf';
13
+ *
14
+ * async function loginHandler(request: NextRequest) {
15
+ * // Your login logic here
16
+ * return NextResponse.json({ success: true });
17
+ * }
18
+ *
19
+ * export const POST = withCsrf(loginHandler);
20
+ * ```
21
+ */
22
+ export declare function withCsrf(handler: (request: NextRequest) => Promise<NextResponse>): (request: NextRequest) => Promise<NextResponse<unknown>>;