@umec/core 0.1.0-alpha.10 → 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.
- package/dist/cli.js +925 -23
- package/dist/cli.js.map +1 -1
- package/package.json +3 -2
- package/registry/disclosure/files/migrations/local/0001_disclosure_requests.sql +23 -0
- package/registry/disclosure/files/src/lib/disclosure-db.ts +214 -0
- package/registry/disclosure/files/src/lib/disclosure-request.ts +818 -0
- package/registry/disclosure/files/src/lib/disclosure-send.ts +231 -0
- package/registry/disclosure/files/src/pages/admin/disclosure.astro +269 -0
- package/registry/disclosure/files/src/pages/api/admin/disclosure-send.ts +11 -0
- package/registry/disclosure/files/src/pages/api/disclosure-request.ts +16 -0
- package/registry/disclosure/files/src/pages/legal/disclosure.astro +135 -0
- package/registry/disclosure/manifest.md +242 -0
- package/registry/example-hello/files/src/example-hello.txt +1 -0
- package/registry/example-hello/manifest.md +10 -0
|
@@ -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
|
+
}
|