@umec/core 0.1.0-alpha.11 → 0.1.0-alpha.12

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
+ import type { ShopDatabase } from '@umec/core/db';
2
+
3
+ export type DisclosureRequestStatus = 'received' | 'disclosed';
4
+
5
+ export type DisclosureRequestRow = {
6
+ id: string;
7
+ requester_email: string;
8
+ status: DisclosureRequestStatus;
9
+ received_at: number;
10
+ disclosed_at: number | null;
11
+ disclosure_email_id: string | null;
12
+ send_attempts: number;
13
+ receipt_email_id: string | null;
14
+ operator_notified_at: number | null;
15
+ ip_hash: string | null;
16
+ user_agent: string | null;
17
+ created_at: number;
18
+ };
19
+
20
+ const SELECT_COLUMNS = `id, requester_email, status, received_at, disclosed_at, disclosure_email_id,
21
+ send_attempts, receipt_email_id, operator_notified_at, ip_hash, user_agent, created_at`;
22
+
23
+ function asCount(row: { count?: unknown } | null): number {
24
+ const value = row?.count;
25
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
26
+ if (typeof value === 'string' && value.trim() !== '') {
27
+ const parsed = Number(value);
28
+ return Number.isFinite(parsed) ? parsed : 0;
29
+ }
30
+ return 0;
31
+ }
32
+
33
+ export async function insertDisclosureRequest(
34
+ db: ShopDatabase,
35
+ input: {
36
+ id: string;
37
+ requesterEmail: string;
38
+ receivedAt: number;
39
+ ipHash: string | null;
40
+ userAgent: string | null;
41
+ },
42
+ ): Promise<void> {
43
+ await db
44
+ .prepare(
45
+ `INSERT INTO disclosure_requests (
46
+ id, requester_email, status, received_at, disclosed_at, disclosure_email_id,
47
+ send_attempts, receipt_email_id, operator_notified_at, ip_hash, user_agent, created_at
48
+ ) VALUES (?, ?, 'received', ?, NULL, NULL, 0, NULL, NULL, ?, ?, ?)`,
49
+ )
50
+ .bind(
51
+ input.id,
52
+ input.requesterEmail,
53
+ input.receivedAt,
54
+ input.ipHash,
55
+ input.userAgent,
56
+ input.receivedAt,
57
+ )
58
+ .run();
59
+ }
60
+
61
+ export async function countRequestsByEmailSince(
62
+ db: ShopDatabase,
63
+ email: string,
64
+ since: number,
65
+ ): Promise<number> {
66
+ const row = await db
67
+ .prepare(
68
+ `SELECT COUNT(*) AS count FROM disclosure_requests
69
+ WHERE requester_email = ? AND received_at >= ?`,
70
+ )
71
+ .bind(email, since)
72
+ .first<{ count: number }>();
73
+ return asCount(row);
74
+ }
75
+
76
+ export async function countRequestsByIpSince(
77
+ db: ShopDatabase,
78
+ ipHash: string,
79
+ since: number,
80
+ ): Promise<number> {
81
+ const row = await db
82
+ .prepare(
83
+ `SELECT COUNT(*) AS count FROM disclosure_requests
84
+ WHERE ip_hash = ? AND received_at >= ?`,
85
+ )
86
+ .bind(ipHash, since)
87
+ .first<{ count: number }>();
88
+ return asCount(row);
89
+ }
90
+
91
+ export async function countRequestsSince(db: ShopDatabase, since: number): Promise<number> {
92
+ const row = await db
93
+ .prepare(`SELECT COUNT(*) AS count FROM disclosure_requests WHERE received_at >= ?`)
94
+ .bind(since)
95
+ .first<{ count: number }>();
96
+ return asCount(row);
97
+ }
98
+
99
+ export async function markDisclosed(
100
+ db: ShopDatabase,
101
+ id: string,
102
+ input: { disclosedAt: number; disclosureEmailId: string },
103
+ ): Promise<void> {
104
+ await db
105
+ .prepare(
106
+ `UPDATE disclosure_requests
107
+ SET status = 'disclosed', disclosed_at = ?, disclosure_email_id = ?
108
+ WHERE id = ?`,
109
+ )
110
+ .bind(input.disclosedAt, input.disclosureEmailId, id)
111
+ .run();
112
+ }
113
+
114
+ export async function incrementSendAttempts(db: ShopDatabase, id: string): Promise<number> {
115
+ await db
116
+ .prepare(`UPDATE disclosure_requests SET send_attempts = send_attempts + 1 WHERE id = ?`)
117
+ .bind(id)
118
+ .run();
119
+ const row = await db
120
+ .prepare(`SELECT send_attempts FROM disclosure_requests WHERE id = ?`)
121
+ .bind(id)
122
+ .first<{ send_attempts: number }>();
123
+ return Number(row?.send_attempts ?? 0);
124
+ }
125
+
126
+ export async function markOperatorNotified(
127
+ db: ShopDatabase,
128
+ id: string,
129
+ notifiedAt: number,
130
+ ): Promise<void> {
131
+ await db
132
+ .prepare(`UPDATE disclosure_requests SET operator_notified_at = ? WHERE id = ?`)
133
+ .bind(notifiedAt, id)
134
+ .run();
135
+ }
136
+
137
+ export async function listRetryableUndisclosed(
138
+ db: ShopDatabase,
139
+ input: { maxAttempts: number; receivedAtOnOrBefore: number; limit?: number },
140
+ ): Promise<DisclosureRequestRow[]> {
141
+ const limit = input.limit ?? 25;
142
+ const result = await db
143
+ .prepare(
144
+ `SELECT ${SELECT_COLUMNS}
145
+ FROM disclosure_requests
146
+ WHERE disclosure_email_id IS NULL
147
+ AND send_attempts < ?
148
+ AND received_at <= ?
149
+ ORDER BY received_at ASC
150
+ LIMIT ?`,
151
+ )
152
+ .bind(input.maxAttempts, input.receivedAtOnOrBefore, limit)
153
+ .all<DisclosureRequestRow>();
154
+ return result.results ?? [];
155
+ }
156
+
157
+ export async function getDisclosureRequest(
158
+ db: ShopDatabase,
159
+ id: string,
160
+ ): Promise<DisclosureRequestRow | null> {
161
+ const row = await db
162
+ .prepare(`SELECT ${SELECT_COLUMNS} FROM disclosure_requests WHERE id = ?`)
163
+ .bind(id)
164
+ .first<DisclosureRequestRow>();
165
+ return row ?? null;
166
+ }
167
+
168
+ export async function listDisclosureRequests(
169
+ db: ShopDatabase,
170
+ input: { limit?: number } = {},
171
+ ): Promise<DisclosureRequestRow[]> {
172
+ const limit = input.limit ?? 100;
173
+ const result = await db
174
+ .prepare(
175
+ `SELECT ${SELECT_COLUMNS}
176
+ FROM disclosure_requests
177
+ ORDER BY CASE WHEN disclosure_email_id IS NULL THEN 0 ELSE 1 END,
178
+ received_at DESC
179
+ LIMIT ?`,
180
+ )
181
+ .bind(limit)
182
+ .all<DisclosureRequestRow>();
183
+ return result.results ?? [];
184
+ }
185
+
186
+ export async function listUndisclosed(
187
+ db: ShopDatabase,
188
+ input: { limit?: number } = {},
189
+ ): Promise<DisclosureRequestRow[]> {
190
+ const limit = input.limit ?? 100;
191
+ const result = await db
192
+ .prepare(
193
+ `SELECT ${SELECT_COLUMNS}
194
+ FROM disclosure_requests
195
+ WHERE disclosure_email_id IS NULL
196
+ ORDER BY received_at ASC
197
+ LIMIT ?`,
198
+ )
199
+ .bind(limit)
200
+ .all<DisclosureRequestRow>();
201
+ return result.results ?? [];
202
+ }
203
+
204
+ export async function listUndisclosedForReminder(db: ShopDatabase): Promise<DisclosureRequestRow[]> {
205
+ const result = await db
206
+ .prepare(
207
+ `SELECT ${SELECT_COLUMNS}
208
+ FROM disclosure_requests
209
+ WHERE disclosure_email_id IS NULL
210
+ ORDER BY received_at ASC`,
211
+ )
212
+ .all<DisclosureRequestRow>();
213
+ return result.results ?? [];
214
+ }