Releases: oddbit/shrtnr
Release list
app-v0.36.1
Maintenance release (PRs #27, #28). No new endpoints or schema changes.
- Dependency refresh. Upgrades all app and tooling dependencies to their latest versions and pins the build to Node >=22.18.0.
- Type-narrowed background handlers. Handlers that only schedule deferred work now accept a
WaitUntilContext(Pick<ExecutionContext, "waitUntil">) instead of the fullExecutionContext, so Hono'sc.executionCtxpasses directly and the prior unsafe casts drop out. - UI fixes. Bundle overview cards clamp titles to two lines and reserve that height so cards stay aligned. The API keys page corrects scope-pill spacing.
- OpenAPI paths and schemas are unchanged from 0.36.0; only
info.versionchanges. The bump refreshes the recorded spec hash in all three SDKs, with no SDK code changes. Full suite: 72 files, 1042 tests.
pub-v2.1.0
Add links.breakdown and bundles.breakdown for paging through the countries, sources and domains analytics panels (offset/limit, returns items + total).
app-v0.36.0
Feature release. Rebuilds the admin dashboard on htmx widget islands, redesigns the API keys page, enriches bundle overview cards, and adds a paginated analytics breakdown endpoint. PRs #16, #18, #19, #21, #22, #24, #25, #26.
- Widget-island dashboard. The dashboard renders as a shell of placeholders and loads each panel (KPIs, timeline, top links, top countries, top domains, recent links) as an independent widget over htmx (vendored 2.0.4). A generic admin widget route serves each fragment with per-widget Cache API caching keyed by range, language, and filters, invalidated by a KV version token. Admin writes bump that token best-effort so panels refresh after an edit. The widget context builds from a single settings fetch per request.
- API keys page redesigned for onboarding. The keys page walks a developer from key creation to first call and lists the published SDKs through a shared
SdkListcomponent. - Bundle overview cards show average clicks per day and traffic coverage, aligned with the bundle detail page.
- Paginated breakdown.
linksandbundlesexpose abreakdownendpoint for the countries, sources, and domains panels (offset/limit, returns items plus total) with a deterministic tie-break in the ordering, and the admin detail pages paginate those panels. The client method shipped earlier in the SDKs (npm/py 1.1.0, Dart 2.1.0). - Fixes. The dashboard counts distinct referrer hosts exactly and shows the primary slug in the most-clicked panel. Dashboard link deltas enrich in bulk to cut per-request CPU. The widget error fragment escapes interpolated values, cache-key parts are url-encoded, and disabled links stay resolvable for up to one second after disabling. Slug copy moved to a delegated
data-*handler that skips empty targets. - OpenAPI paths and schemas are unchanged from 0.35.5 (the breakdown endpoint was already in the recorded spec); only
info.versionchanges. The bump refreshes the recorded spec hash in all three SDKs, with no SDK code changes. Full suite: 72 files, 1042 tests.
py-v1.1.0
- Add
links.breakdownandbundles.breakdown(sync and async) for paging through the countries, sources and domains analytics panels (offset/limit, returns items + total).
npm-v1.1.0
- Add
links.breakdownandbundles.breakdownfor paging through the countries, sources and domains analytics panels (offset/limit, returns{ items, total }). Exports new typesBreakdownDimensionandBreakdownPage.
app-v0.35.5
Defect-review release (PR #15) fixing analytics "Sources" inflation. No new features.
- Shortlinks no longer appear as referring sources for their own clicks. Self-referrer detection broadened from the bare-origin root to any same-host slug: a slug is never a page, it only 301s away, so a
Refererpointing at one can only be a self-referral artifact (a self-looping or cross-slug crawler that stampsRefererwith the URL it fetched). Such clicks now carry theis_self_referrerflag and drop out of the Sources breakdown and totals under the default filters. - The rule matches the actual slug route shape (a single path segment outside the reserved
_namespace), so real same-host endpoints like/.well-known/oauth-authorization-serverand/cdn-cgi/access/*stay as meaningful referrers. The own host is read per request, so no domain is hardcoded and every deployment works. - OpenAPI surface unchanged. The version bump refreshes the recorded spec hash in all three SDKs; no SDK code changes. Full suite: 50 files, 953 tests.
app-v0.35.4
Defect-review release (PR #14) extending the 0.35.3 concurrency hardening to slug removal and disable. No new features.
- Removing a slug no longer orphans analytics rows when a click lands mid-operation. The lifetime guard moved inside the delete transaction as a
NOT EXISTS (SELECT 1 FROM clicks ...)condition, so a click recorded between the service's pre-read and the batch delete blocks the delete instead of stranding its click rows. - Removing or disabling a primary slug no longer leaves a link with two primaries under concurrency. The primary handover re-checks inside the batch that the slug still holds primary, so a concurrent
setPrimarythat moved primary to another slug can no longer promote the random slug alongside the new primary. removeSlugreports the real outcome instead of a false success. When the guard blocks the delete, the API returns 400 (slug still present, has clicks) or 404 (slug left this link concurrently) rather than{ removed: true }, and the cache entry is evicted only after a confirmed delete so a still-resolving slug is not dropped. The 404-vs-400 disambiguation compareslink_id, since a freed slug value can be re-claimed by another link.- OpenAPI surface unchanged. The version bump refreshes the recorded spec hash in all three SDKs; no SDK code changes. Full suite: 50 files, 938 tests.
app-v0.35.3
Robustness release: two defect-review PRs (#12, #13) hardening concurrency, input validation, and redirect behavior. No new features.
- Slug redirects now send
Cache-Control: private, max-age=90alongside the 301. A bare 301 is cached by browsers indefinitely, so returning visitors skipped the Worker forever: disabling, expiring, or retargeting a link never reached them and their repeat clicks went unrecorded. The short private max-age (the Bitly approach) keeps the SEO semantics while forcing revalidation within seconds. expires_at = 0now reads as a real epoch timestamp (expired) instead of "no expiry". The redirect handler and the admin pages (expired filter, expired badge, expiry control) use null-aware checks, matching the documented contract that null means no expiry.- Multi-statement mutations run in transactional D1 batches: link deletion, custom slug insertion with primary handover, slug disable/remove primary fallback. A mid-sequence failure can no longer orphan slug or click rows or strand a link without a primary slug.
setPrimarycollapsed to a single conditional UPDATE that no-ops when the slug does not belong to the link; previously a mismatched slug cleared every primary flag and set none. - The "never delete a link with clicks" guard moved inside the delete transaction as a
NOT EXISTScondition. A click recorded between the service's check and the delete now blocks the delete instead of being silently removed with the link. - Concurrent requests racing on the same custom slug get a 409 from the UNIQUE constraint instead of an unhandled 500.
disableSlug/enableSlugreturn 404 instead of crashing when the slug is concurrently deleted. - Link search escapes SQLite LIKE metacharacters. Searching for
_previously matched every link;%and\are escaped the same way and every comparison carriesESCAPE '\'. - MCP
get_link_qrencodesutm_medium=qrinstead of a bare?qr, so scans of MCP-issued QR codes register as QR traffic in analytics, matching the REST QR endpoint. - Settings validation: theme and lang are checked against their allowed sets on write and clamped to null on read (legacy rows and direct D1 edits no longer leak unknown values), API key titles cap at 120 characters, and a corrupted stored
slug_default_lengthfalls back to the default instead of returning NaN or blocking link creation. - The admin JSON path (no zod schema) now rejects malformed field types at the service layer: non-string labels and non-integer or negative
expires_atvalues return 400 instead of being stored verbatim. getBundleapplies the viewer's bot and self-referrer filter preferences, so bundle detail and the MCPget_bundletool report the same totals as the bundles list.- With Cloudflare Access configured, unauthenticated requests to
/_/admin/api/*get 401 JSON instead of a 302 redirect to the landing page; page routes keep the redirect. Theme and lang cookie values clamp to known sets before rendering. - OpenAPI surface unchanged. The version bump refreshes the recorded spec hash in all three SDKs; no SDK code changes. Full suite: 50 files, 932 tests.
app-v0.35.2
- Link deletion no longer reports success when the database guard blocks it.
deleteLinknow honors the boolean fromLinkRepository.delete(): if a click lands between the service's click check and the row delete, the API returns the real outcome instead of{ deleted: true }. A link that gained clicks returns 400, a link that was concurrently removed returns 404. The existence check on that path uses a cheapSELECT 1rather than a full link load. - Deleting a link evicts the slug set captured at delete time, not the slugs read at the start of the call. A custom slug added in the race window is now removed from KV instead of lingering with no TTL, which had kept the deleted link resolving on redirects.
disableLinkreturns 404 when the link is concurrently deleted instead of throwing a 500. It now null-guards the repository result the same wayenableLinkalready did.- Dependency refresh:
wrangler 4.88 → 4.95,hono 4.12.18 → 4.12.23,agents 0.12.3 → 0.13.3,@cloudflare/workers-oauth-provider 0.5 → 0.7,@hono/zod-openapi 1.3 → 1.4,@cloudflare/vitest-pool-workers 0.16.0 → 0.16.10,@cloudflare/workers-types → 4.20260528. - CI reads the Node version from
package.json(pinned to 22) instead of hardcoding 24, so the tested major matches the supported floor. - OpenAPI surface unchanged. The version bump refreshes the recorded spec hash in all three SDKs; no SDK code changes.
py-v1.0.2
links.update()andbundles.update()can now distinguish "omit this field" from "set this field to null". Optional nullable params (label,expires_at,description,icon) default to a privateUNSETsentinel; only keys explicitly provided by the caller are included in the request body. PassNoneto clear a value, omit the param to leave the server-side value untouched. Sync and async resources both updated; regression tests cover both clients.