@press2ai/engine 0.4.0 → 0.4.1
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/package.json +1 -1
- package/src/ceidg-vertical.ts +27 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@press2ai/engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Multi-tenant runtime + template contracts dla otwarty-* verticali. SSR na Cloudflare Workers, isomorphic renderer (SSG/browser w roadmapie).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/ceidg-vertical.ts
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
* i — fix względem starego trener-landinga — `cities` query też filtruje
|
|
23
23
|
* po kategorii (wcześniej trener pokazywał miasta z całej tabeli leads,
|
|
24
24
|
* mieszając wszystkie wertykale).
|
|
25
|
-
* - Opt-out jest **globalny**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* - Opt-out jest **globalny** — UPDATE leads bez filtra category. RODO
|
|
26
|
+
* sprzeciw to usunięcie danych osobowych, nie selektywne ukrycie w jednej
|
|
27
|
+
* kategorii. Soft flag + filtr w queries — hard delete bez proof-of-ownership
|
|
28
|
+
* jest podatny na sabotage (patrz NEXT-STEPS manifest-1/manifest-2 notatka).
|
|
29
29
|
*
|
|
30
30
|
* Hono jest peer dependency (każdy wertykal i tak go ma). Engine core nie
|
|
31
31
|
* importuje Hono — tylko ten subpath go używa.
|
|
@@ -54,6 +54,7 @@ export type CeidgLead = {
|
|
|
54
54
|
claimed: number;
|
|
55
55
|
external_site_url: string | null;
|
|
56
56
|
fetched_at: number;
|
|
57
|
+
opted_out_at: number | null;
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
export type CeidgBindings = { DB: D1Database; CACHE: KVNamespace };
|
|
@@ -108,15 +109,17 @@ export function createVerticalApp(config: VerticalConfig): Hono<{ Bindings: Ceid
|
|
|
108
109
|
// ─────────── SQL ───────────
|
|
109
110
|
// EXISTS subselect zamiast JOIN: lead z N PKDs nie multiplikuje wierszy.
|
|
110
111
|
// (SELECT pkd ... LIMIT 1) wybiera jeden PKD per lead — wystarczy do
|
|
111
|
-
// pkdToJobTitle.
|
|
112
|
-
//
|
|
112
|
+
// pkdToJobTitle. To jest świadomy uproszczenie: lead z mieszanymi PKDs
|
|
113
|
+
// (np. fizjoterapia + psychologia) dostanie tylko jeden label.
|
|
113
114
|
const leadsQuery = `
|
|
114
115
|
SELECT l.nip, l.first_name, l.last_name, c.name as city,
|
|
115
116
|
l.company_name, l.slug, l.claimed, l.external_site_url, l.fetched_at,
|
|
117
|
+
l.opted_out_at,
|
|
116
118
|
(SELECT pkd FROM lead_pkd WHERE nip = l.nip LIMIT 1) as pkd
|
|
117
119
|
FROM leads l
|
|
118
120
|
LEFT JOIN cities c ON l.city_id = c.id
|
|
119
|
-
WHERE
|
|
121
|
+
WHERE l.opted_out_at IS NULL
|
|
122
|
+
AND EXISTS (SELECT 1 FROM lead_categories WHERE lead_nip = l.nip AND category = ?)`;
|
|
120
123
|
|
|
121
124
|
// Cities query filtruje po kategorii — fix względem starego trener landinga
|
|
122
125
|
// który pokazywał miasta z całej tabeli leads (mieszane wertykale).
|
|
@@ -125,6 +128,7 @@ export function createVerticalApp(config: VerticalConfig): Hono<{ Bindings: Ceid
|
|
|
125
128
|
FROM leads l
|
|
126
129
|
JOIN cities c ON l.city_id = c.id
|
|
127
130
|
INNER JOIN lead_categories lc ON l.nip = lc.lead_nip AND lc.category = ?
|
|
131
|
+
WHERE l.opted_out_at IS NULL
|
|
128
132
|
GROUP BY c.name ORDER BY count DESC LIMIT 8`;
|
|
129
133
|
|
|
130
134
|
// ─────────── Content loader ───────────
|
|
@@ -258,15 +262,10 @@ export function createVerticalApp(config: VerticalConfig): Hono<{ Bindings: Ceid
|
|
|
258
262
|
}
|
|
259
263
|
app.get('/opt-out', (c) => c.html(renderStatic(pages.optOutForm)));
|
|
260
264
|
|
|
261
|
-
// One-click opt-out: przycisk na karcie profilu POST-uje slug. Hard delete
|
|
262
|
-
// z `leads` (cascade do `lead_pkd`/`lead_categories`) + insert do blacklisty
|
|
263
|
-
// `opted_out_nips` żeby sync nie wprowadził NIP-a z powrotem. Rate limit
|
|
264
|
-
// 5/h/IP przeciw abuse — slug jest publiczny, ktokolwiek może nukeować,
|
|
265
|
-
// ale katalog to publiczne wizytówki z CEIDG, nie konta.
|
|
266
265
|
app.post('/opt-out', async (c) => {
|
|
267
266
|
const form = await c.req.formData();
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
267
|
+
const nip = String(form.get('nip') ?? '').replace(/\D/g, '');
|
|
268
|
+
if (nip.length !== 10) return c.text('Nieprawidłowy NIP', 400);
|
|
270
269
|
|
|
271
270
|
const ip = c.req.header('cf-connecting-ip') ?? '0.0.0.0';
|
|
272
271
|
const rlKey = `vertical:${category}:rl:optout:${ip}`;
|
|
@@ -274,21 +273,20 @@ export function createVerticalApp(config: VerticalConfig): Hono<{ Bindings: Ceid
|
|
|
274
273
|
if (rlVal >= OPTOUT_LIMIT) return c.text('Zbyt wiele prób. Spróbuj ponownie za godzinę.', 429);
|
|
275
274
|
await c.env.CACHE.put(rlKey, String(rlVal + 1), { expirationTtl: OPTOUT_WINDOW_SEC });
|
|
276
275
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
276
|
+
// Opt-out jest GLOBALNY — usuwa też z innych wertykali. RODO sprzeciw to
|
|
277
|
+
// usunięcie danych osobowych, nie selektywne ukrycie w jednej kategorii.
|
|
278
|
+
// Soft flag (nie DELETE) — defense in depth przeciw masowej sabotage:
|
|
279
|
+
// catalog.json emituje slug+imię+miasto bez NIP, więc adversary musi
|
|
280
|
+
// zmapować slug→NIP przez CEIDG pod rate limit (6000 req/h); dodatkowo
|
|
281
|
+
// soft flag jest natychmiast odwracalny jednym UPDATE gdyby coś poszło nie tak.
|
|
282
|
+
const res = await c.env.DB
|
|
283
|
+
.prepare(
|
|
284
|
+
'UPDATE leads SET opted_out_at = ?, opted_out_ip = ?, opted_out_ua = ? WHERE nip = ? AND opted_out_at IS NULL',
|
|
285
|
+
)
|
|
286
|
+
.bind(Math.floor(Date.now() / 1000), ip, c.req.header('user-agent') ?? '', nip)
|
|
287
|
+
.run();
|
|
288
|
+
|
|
289
|
+
if (!res.meta.changes) return c.text('Nie znaleziono wpisu', 404);
|
|
292
290
|
return c.html(renderStatic(pages.optOutDone));
|
|
293
291
|
});
|
|
294
292
|
|
|
@@ -310,8 +308,3 @@ export function createVerticalApp(config: VerticalConfig): Hono<{ Bindings: Ceid
|
|
|
310
308
|
|
|
311
309
|
return app;
|
|
312
310
|
}
|
|
313
|
-
|
|
314
|
-
async function sha256Hex(input: string): Promise<string> {
|
|
315
|
-
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
|
|
316
|
-
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
317
|
-
}
|