emdash 0.5.0 → 0.7.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/dist/{adapters-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
- package/dist/adapters-Di31kZ28.d.mts.map +1 -0
- package/dist/{apply-Cma_PiF6.mjs → apply-5uslYdUu.mjs} +197 -25
- package/dist/apply-5uslYdUu.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +203 -33
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +30 -4
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +11 -4
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +467 -186
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +17 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
- package/dist/byline-C4OVd8b3.mjs.map +1 -0
- package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
- package/dist/bylines-hPTW79hw.mjs.map +1 -0
- package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
- package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
- package/dist/chunks-HGz06Soa.mjs +19 -0
- package/dist/chunks-HGz06Soa.mjs.map +1 -0
- package/dist/cli/index.mjs +12 -11
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{config-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
- package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
- package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
- package/dist/connection-2igzM-AT.mjs.map +1 -0
- package/dist/{content-BsBoyj8G.mjs → content-D7J5y73J.mjs} +27 -1
- package/dist/{content-BsBoyj8G.mjs.map → content-D7J5y73J.mjs.map} +1 -1
- package/dist/database/instrumentation.d.mts +45 -0
- package/dist/database/instrumentation.d.mts.map +1 -0
- package/dist/database/instrumentation.mjs +61 -0
- package/dist/database/instrumentation.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/index.mjs.map +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db-errors-D0UT85nC.mjs +41 -0
- package/dist/db-errors-D0UT85nC.mjs.map +1 -0
- package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
- package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
- package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
- package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
- package/dist/{index-CCWzlriB.d.mts → index-De6_Xv3v.d.mts} +209 -19
- package/dist/index-De6_Xv3v.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +23 -21
- package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
- package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
- package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
- package/dist/loader-DeiBJEMe.mjs.map +1 -0
- package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
- package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
- package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
- package/dist/page/index.d.mts +11 -2
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +23 -1
- package/dist/page/index.mjs.map +1 -1
- package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
- package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.d.mts.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-B6Vu0d2i.mjs → query-g4Ug-9j9.mjs} +79 -12
- package/dist/query-g4Ug-9j9.mjs.map +1 -0
- package/dist/{redirect-7lGhLBNZ.mjs → redirect-CN0Rt9Ob.mjs} +66 -10
- package/dist/redirect-CN0Rt9Ob.mjs.map +1 -0
- package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
- package/dist/registry-Ci3WxVAr.mjs.map +1 -0
- package/dist/request-cache-DiR961CV.mjs +79 -0
- package/dist/request-cache-DiR961CV.mjs.map +1 -0
- package/dist/request-context.d.mts +19 -16
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-DYv3rX8P.d.mts → runner-BR2xKwhn.d.mts} +2 -2
- package/dist/{runner-DYv3rX8P.d.mts.map → runner-BR2xKwhn.d.mts.map} +1 -1
- package/dist/{runner-Cd-_WyDo.mjs → runner-tQ7BJ4T7.mjs} +211 -134
- package/dist/runner-tQ7BJ4T7.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +1 -1
- package/dist/{search-Cn1SYvYF.mjs → search-B0effn3j.mjs} +210 -226
- package/dist/search-B0effn3j.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +10 -9
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/taxonomies-K2z0Uhnj.mjs +308 -0
- package/dist/taxonomies-K2z0Uhnj.mjs.map +1 -0
- package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
- package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
- package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
- package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
- package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
- package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
- package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
- package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
- package/dist/{types-B6BzlZxx.d.mts → types-C2v0c34j.d.mts} +10 -1
- package/dist/{types-B6BzlZxx.d.mts.map → types-C2v0c34j.d.mts.map} +1 -1
- package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
- package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
- package/dist/{types-DeG21anB.d.mts → types-CnZYHyLW.d.mts} +55 -5
- package/dist/types-CnZYHyLW.d.mts.map +1 -0
- package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
- package/dist/types-DDS4MxsT.mjs.map +1 -0
- package/dist/{types-C3ronwXb.d.mts → types-DgrIP0tF.d.mts} +102 -4
- package/dist/types-DgrIP0tF.d.mts.map +1 -0
- package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
- package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
- package/dist/{validate-Db1yNL3i.d.mts → validate-kM8Pjuf7.d.mts} +5 -52
- package/dist/validate-kM8Pjuf7.d.mts.map +1 -0
- package/dist/version-BnTKdfam.mjs +7 -0
- package/dist/{version-CMMjTuqu.mjs.map → version-BnTKdfam.mjs.map} +1 -1
- package/package.json +10 -5
- package/src/after.ts +62 -0
- package/src/api/handlers/content.ts +2 -0
- package/src/api/handlers/oauth-authorization.ts +2 -32
- package/src/api/handlers/oauth-clients.ts +40 -4
- package/src/api/handlers/taxonomies.ts +13 -0
- package/src/api/oauth/redirect-uri.ts +34 -0
- package/src/api/openapi/document.ts +126 -118
- package/src/api/schemas/content.ts +8 -0
- package/src/api/schemas/media.ts +26 -15
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/integration/font-provider.ts +178 -0
- package/src/astro/integration/index.ts +44 -0
- package/src/astro/integration/routes.ts +6 -0
- package/src/astro/integration/runtime.ts +117 -0
- package/src/astro/integration/virtual-modules.ts +41 -39
- package/src/astro/integration/vite-config.ts +16 -5
- package/src/astro/middleware/auth.ts +33 -1
- package/src/astro/middleware/request-context.ts +15 -3
- package/src/astro/middleware.ts +340 -263
- package/src/astro/routes/admin.astro +21 -10
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
- package/src/astro/routes/api/auth/signup/request.ts +26 -8
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +10 -6
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +5 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +26 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +30 -2
- package/src/astro/routes/api/content/[collection]/index.ts +19 -1
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +5 -4
- package/src/astro/routes/api/manifest.ts +7 -0
- package/src/astro/routes/api/media/upload-url.ts +10 -2
- package/src/astro/routes/api/media.ts +10 -7
- package/src/astro/routes/api/oauth/device/code.ts +2 -1
- package/src/astro/routes/api/oauth/device/token.ts +2 -1
- package/src/astro/routes/api/oauth/register.ts +178 -0
- package/src/astro/routes/api/oauth/token.ts +15 -0
- package/src/astro/routes/api/openapi.json.ts +15 -5
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
- package/src/astro/routes/api/search/index.ts +5 -0
- package/src/astro/routes/api/search/suggest.ts +3 -0
- package/src/astro/routes/api/setup/admin-verify.ts +30 -5
- package/src/astro/routes/api/setup/admin.ts +32 -8
- package/src/astro/routes/api/setup/index.ts +5 -2
- package/src/astro/routes/api/taxonomies/index.ts +1 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +1 -1
- package/src/astro/types.ts +9 -0
- package/src/auth/rate-limit.ts +50 -22
- package/src/auth/setup-nonce.ts +22 -0
- package/src/auth/trusted-proxy.ts +92 -0
- package/src/bylines/index.ts +22 -45
- package/src/components/EmDashHead.astro +23 -7
- package/src/database/connection.ts +23 -1
- package/src/database/instrumentation.ts +98 -0
- package/src/database/migrations/035_bounded_404_log.ts +112 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +39 -0
- package/src/database/repositories/options.ts +25 -0
- package/src/database/repositories/redirect.ts +111 -8
- package/src/database/types.ts +9 -0
- package/src/db/adapters.ts +15 -0
- package/src/emdash-runtime.ts +312 -92
- package/src/import/registry.ts +4 -3
- package/src/import/ssrf.ts +253 -12
- package/src/index.ts +6 -0
- package/src/loader.ts +19 -24
- package/src/mcp/server.ts +76 -3
- package/src/menus/index.ts +6 -3
- package/src/page/index.ts +1 -1
- package/src/page/seo-contributions.ts +36 -0
- package/src/plugins/context.ts +15 -3
- package/src/plugins/manager.ts +6 -0
- package/src/plugins/request-meta.ts +66 -15
- package/src/plugins/routes.ts +3 -1
- package/src/query.ts +104 -7
- package/src/request-cache.ts +106 -0
- package/src/request-context.ts +19 -0
- package/src/schema/query.ts +5 -2
- package/src/schema/registry.ts +243 -166
- package/src/schema/types.ts +13 -2
- package/src/schema/zod-generator.ts +4 -0
- package/src/search/fts-manager.ts +19 -5
- package/src/search/query.ts +4 -3
- package/src/seed/apply.ts +41 -1
- package/src/settings/index.ts +24 -5
- package/src/taxonomies/index.ts +324 -124
- package/src/utils/db-errors.ts +46 -0
- package/src/virtual-modules.d.ts +31 -10
- package/src/visual-editing/toolbar.ts +6 -1
- package/src/widgets/index.ts +54 -25
- package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
- package/dist/apply-Cma_PiF6.mjs.map +0 -1
- package/dist/byline-WuOq9MFJ.mjs.map +0 -1
- package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
- package/dist/connection-B4zVnQIa.mjs.map +0 -1
- package/dist/index-CCWzlriB.d.mts.map +0 -1
- package/dist/loader-BYzwzORf.mjs.map +0 -1
- package/dist/query-B6Vu0d2i.mjs.map +0 -1
- package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
- package/dist/registry-BgnP3ysR.mjs.map +0 -1
- package/dist/runner-Cd-_WyDo.mjs.map +0 -1
- package/dist/search-Cn1SYvYF.mjs.map +0 -1
- package/dist/types-C3ronwXb.d.mts.map +0 -1
- package/dist/types-DeG21anB.d.mts.map +0 -1
- package/dist/types-xxCWI3j0.mjs.map +0 -1
- package/dist/validate-Db1yNL3i.d.mts.map +0 -1
- package/dist/version-CMMjTuqu.mjs +0 -7
|
@@ -11,6 +11,32 @@ import { currentTimestampValue } from "../dialect-helpers.js";
|
|
|
11
11
|
import type { Database, RedirectTable } from "../types.js";
|
|
12
12
|
import { encodeCursor, decodeCursor, type FindManyResult } from "./types.js";
|
|
13
13
|
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Bounded 404 logging
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hard cap on rows stored in `_emdash_404_log`. When exceeded, the oldest
|
|
20
|
+
* rows (by `last_seen_at`) are evicted on insert. Prevents an unauthenticated
|
|
21
|
+
* attacker from growing the table without bound by requesting unique URLs.
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_404_LOG_ROWS = 10_000;
|
|
24
|
+
|
|
25
|
+
/** Max stored length for the `Referer` header — truncated on insert. */
|
|
26
|
+
export const REFERRER_MAX_LENGTH = 512;
|
|
27
|
+
|
|
28
|
+
/** Max stored length for the `User-Agent` header — truncated on insert. */
|
|
29
|
+
export const USER_AGENT_MAX_LENGTH = 256;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Truncate a header-derived string to `max` chars, preserving `null`/`undefined`
|
|
33
|
+
* as `null`. Empty strings stay empty (the caller decides whether to coerce).
|
|
34
|
+
*/
|
|
35
|
+
function truncateOrNull(value: string | null | undefined, max: number): string | null {
|
|
36
|
+
if (value === null || value === undefined) return null;
|
|
37
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
// ---------------------------------------------------------------------------
|
|
15
41
|
// Types
|
|
16
42
|
// ---------------------------------------------------------------------------
|
|
@@ -369,22 +395,97 @@ export class RedirectRepository {
|
|
|
369
395
|
|
|
370
396
|
// --- 404 log ------------------------------------------------------------
|
|
371
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Record a 404 hit for `entry.path`.
|
|
400
|
+
*
|
|
401
|
+
* Dedups by path: repeat hits increment `hits` and refresh `last_seen_at`
|
|
402
|
+
* on the existing row instead of inserting a new one. Referrer and
|
|
403
|
+
* user-agent are truncated to bounded lengths so a malicious client can't
|
|
404
|
+
* blow up storage with huge headers. When the table would exceed
|
|
405
|
+
* MAX_404_LOG_ROWS, the oldest entries (by `last_seen_at`) are evicted.
|
|
406
|
+
*
|
|
407
|
+
* This is called from the public redirect middleware on every 404 and
|
|
408
|
+
* must never throw for an unauthenticated caller — failures bubble up to
|
|
409
|
+
* the middleware, which swallows them.
|
|
410
|
+
*/
|
|
372
411
|
async log404(entry: {
|
|
373
412
|
path: string;
|
|
374
413
|
referrer?: string | null;
|
|
375
414
|
userAgent?: string | null;
|
|
376
415
|
ip?: string | null;
|
|
377
416
|
}): Promise<void> {
|
|
417
|
+
const now = new Date().toISOString();
|
|
418
|
+
const referrer = truncateOrNull(entry.referrer, REFERRER_MAX_LENGTH);
|
|
419
|
+
const userAgent = truncateOrNull(entry.userAgent, USER_AGENT_MAX_LENGTH);
|
|
420
|
+
const ip = entry.ip ?? null;
|
|
421
|
+
|
|
422
|
+
// Atomic upsert by path. The UNIQUE index on `path` makes this safe
|
|
423
|
+
// under concurrency: two requests for the same new path can't both
|
|
424
|
+
// insert — the second one hits the conflict branch and increments
|
|
425
|
+
// hits instead of failing with a uniqueness error.
|
|
378
426
|
await this.db
|
|
379
427
|
.insertInto("_emdash_404_log")
|
|
380
428
|
.values({
|
|
381
429
|
id: ulid(),
|
|
382
430
|
path: entry.path,
|
|
383
|
-
referrer
|
|
384
|
-
user_agent:
|
|
385
|
-
ip
|
|
386
|
-
|
|
431
|
+
referrer,
|
|
432
|
+
user_agent: userAgent,
|
|
433
|
+
ip,
|
|
434
|
+
hits: 1,
|
|
435
|
+
last_seen_at: now,
|
|
436
|
+
created_at: now,
|
|
387
437
|
})
|
|
438
|
+
.onConflict((oc) =>
|
|
439
|
+
oc.column("path").doUpdateSet({
|
|
440
|
+
hits: sql`hits + 1`,
|
|
441
|
+
last_seen_at: now,
|
|
442
|
+
referrer,
|
|
443
|
+
user_agent: userAgent,
|
|
444
|
+
ip,
|
|
445
|
+
}),
|
|
446
|
+
)
|
|
447
|
+
.execute();
|
|
448
|
+
|
|
449
|
+
// Enforce the row cap. Cheap when the table is under cap (single
|
|
450
|
+
// COUNT(*) query); evicts oldest rows if we're over. Updates (dedup
|
|
451
|
+
// hits) don't grow the table so this is a no-op for repeat paths.
|
|
452
|
+
await this.enforce404Cap();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Delete the oldest rows from `_emdash_404_log` if the row count exceeds
|
|
457
|
+
* MAX_404_LOG_ROWS. "Oldest" is by `last_seen_at`, so a path that keeps
|
|
458
|
+
* getting hit stays in the table even if it was first seen long ago.
|
|
459
|
+
*
|
|
460
|
+
* Private — callers use `log404`, which invokes this after every upsert.
|
|
461
|
+
*/
|
|
462
|
+
private async enforce404Cap(): Promise<void> {
|
|
463
|
+
const countRow = await this.db
|
|
464
|
+
.selectFrom("_emdash_404_log")
|
|
465
|
+
.select((eb) => eb.fn.countAll<number>().as("c"))
|
|
466
|
+
.executeTakeFirst();
|
|
467
|
+
const count = Number(countRow?.c ?? 0);
|
|
468
|
+
if (count <= MAX_404_LOG_ROWS) return;
|
|
469
|
+
|
|
470
|
+
const excess = count - MAX_404_LOG_ROWS;
|
|
471
|
+
|
|
472
|
+
// Evict the oldest rows in a single SQL statement. Using a subquery
|
|
473
|
+
// (rather than materialising the victim IDs in JS and passing them
|
|
474
|
+
// back as bind parameters) keeps the statement bounded regardless of
|
|
475
|
+
// how far over cap the table is — important for existing installs
|
|
476
|
+
// that crossed the threshold before this cap was introduced.
|
|
477
|
+
await this.db
|
|
478
|
+
.deleteFrom("_emdash_404_log")
|
|
479
|
+
.where(
|
|
480
|
+
"id",
|
|
481
|
+
"in",
|
|
482
|
+
this.db
|
|
483
|
+
.selectFrom("_emdash_404_log")
|
|
484
|
+
.select("id")
|
|
485
|
+
.orderBy("last_seen_at", "asc")
|
|
486
|
+
.orderBy("id", "asc")
|
|
487
|
+
.limit(excess),
|
|
488
|
+
)
|
|
388
489
|
.execute();
|
|
389
490
|
}
|
|
390
491
|
|
|
@@ -438,6 +539,10 @@ export class RedirectRepository {
|
|
|
438
539
|
}
|
|
439
540
|
|
|
440
541
|
async get404Summary(limit = 50): Promise<NotFoundSummary[]> {
|
|
542
|
+
// Since rows are now deduped by path, each path has exactly one row
|
|
543
|
+
// with `hits` as the running count and `last_seen_at` as the latest
|
|
544
|
+
// timestamp. The subquery for `top_referrer` collapses to a simple
|
|
545
|
+
// pick of the row's stored referrer (the most recent one seen).
|
|
441
546
|
const rows = await sql<{
|
|
442
547
|
path: string;
|
|
443
548
|
count: number;
|
|
@@ -446,14 +551,12 @@ export class RedirectRepository {
|
|
|
446
551
|
}>`
|
|
447
552
|
SELECT
|
|
448
553
|
path,
|
|
449
|
-
|
|
450
|
-
MAX(
|
|
554
|
+
SUM(hits) as count,
|
|
555
|
+
MAX(last_seen_at) as last_seen,
|
|
451
556
|
(
|
|
452
557
|
SELECT referrer FROM _emdash_404_log AS inner_log
|
|
453
558
|
WHERE inner_log.path = _emdash_404_log.path
|
|
454
559
|
AND referrer IS NOT NULL AND referrer != ''
|
|
455
|
-
GROUP BY referrer
|
|
456
|
-
ORDER BY COUNT(*) DESC
|
|
457
560
|
LIMIT 1
|
|
458
561
|
) as top_referrer
|
|
459
562
|
FROM _emdash_404_log
|
package/src/database/types.ts
CHANGED
|
@@ -466,6 +466,15 @@ export interface NotFoundLogTable {
|
|
|
466
466
|
referrer: string | null;
|
|
467
467
|
user_agent: string | null;
|
|
468
468
|
ip: string | null;
|
|
469
|
+
hits: number;
|
|
470
|
+
/**
|
|
471
|
+
* Migration 035 adds this as a nullable column (SQLite can't add a
|
|
472
|
+
* NOT NULL column with a non-constant default to an existing table).
|
|
473
|
+
* The `log404` upsert always writes a value, so new and updated rows
|
|
474
|
+
* always have one, but existing rows pre-migration were backfilled
|
|
475
|
+
* without a NOT NULL constraint. Typed as nullable to match the schema.
|
|
476
|
+
*/
|
|
477
|
+
last_seen_at: string | null;
|
|
469
478
|
created_at: string;
|
|
470
479
|
}
|
|
471
480
|
|
package/src/db/adapters.ts
CHANGED
|
@@ -33,6 +33,21 @@ export interface DatabaseDescriptor {
|
|
|
33
33
|
entrypoint: string;
|
|
34
34
|
config: unknown;
|
|
35
35
|
type: DatabaseDialectType;
|
|
36
|
+
/**
|
|
37
|
+
* When true, the adapter's runtime entrypoint MUST export a named
|
|
38
|
+
* `createRequestScopedDb` function matching the signature declared in
|
|
39
|
+
* `virtual:emdash/dialect`. The virtual-module generator re-exports it
|
|
40
|
+
* by name, so a missing export becomes a build-time bundler error.
|
|
41
|
+
*
|
|
42
|
+
* The function is called once per request and decides — based on its own
|
|
43
|
+
* runtime config (e.g. whether the user opted into D1 sessions) — whether
|
|
44
|
+
* to return a per-request Kysely or null. Use this for features like D1
|
|
45
|
+
* read-replica sessions, bookmark cookies, or any per-request DB handle.
|
|
46
|
+
*
|
|
47
|
+
* When false or absent, the generator emits a stub that returns null and
|
|
48
|
+
* the middleware takes its default (singleton) path.
|
|
49
|
+
*/
|
|
50
|
+
supportsRequestScope?: boolean;
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
export interface SqliteConfig {
|