feat: self-contained Deno app mirroring PDS avatar → Gravatar #2

Merged
alex merged 10 commits from pi/issue1-1783629626256 into master 2026-07-10 11:47:02 +00:00
Contributor

Closes #1.

Implements the full plan: a zero-dependency Deno app that keeps a Gravatar profile picture in sync with the account's Bluesky/ATProto avatar, packaged as a podman container.

What's included

Sync logic (lib/ + main.ts)

  • lib/pds.tsPdsClient: public PDS xrpc reads (resolveHandle, getRecord → avatar cid/mime, getBlob). No PDS auth needed.
  • lib/gravatar.tsGravatarClient: list / upload / activate / delete / cleanup + emailHash (SHA-256 of normalised email). Bearer-token auth.
  • lib/state.tsStateStore: persists the last-synced cid on the mounted volume.
  • main.tsloadConfig (env, injectable for tests), sync() orchestration, main() entry.

Each run resolves handle → DID, reads the avatar blob ref, skips if the content-addressed cid is unchanged, else fetches the blob → uploads → activates → best-effort prunes stale images → persists the cid. The freshly uploaded image is protected by identity, so a flaky selected flag can never delete it.

Tests (main_test.ts) — 7 tests, all using an in-process fake fetch (no real network):

  • fresh upload + activate
  • skip on unchanged avatar
  • re-upload + prune previous on change
  • graceful no-op when avatar removed
  • cleanup never deletes the new image even with a flaky selected flag
  • loadConfig missing-vars rejection + defaults/trailing-slash trimming

Packaging & ops

  • Containerfile — multi-stage deno compile → standalone binary on debian:stable-slim (+ ca-certificates).
  • compose.yml — optional local/manual runs.
  • systemd/gravatar-avatar-sync.{service,timer} — oneshot podman run --rm, daily + Persistent=true.
  • .env.example, .gitignore.
  • .forgejo/workflows/ci.ymlfmt --check / lint / check / test + a container build job (the existing pi.yml is untouched).
  • README.md — what it does, env-var table, how to run (podman / compose / systemd), token generation.

Notes

  • Zero deps by design — only Web-platform builtins; no external imports, so there is no deno.lock to commit.
  • Validated locally: deno fmt --check, deno lint, deno check main.ts clean; deno test → 7 passed; deno compile produces a working binary (exits 1 with a clear message on missing env). The systemd units parse cleanly. The container build couldn't run inside this sandbox (buildkit needs catatonit), but the deno compile step it performs was verified directly.
  • Per the issue, scheduling/deployment wiring lives elsewhere — these are the template units.
Closes #1. Implements the full plan: a zero-dependency Deno app that keeps a Gravatar profile picture in sync with the account's Bluesky/ATProto avatar, packaged as a podman container. ## What's included **Sync logic (`lib/` + `main.ts`)** - `lib/pds.ts` — `PdsClient`: public PDS xrpc reads (`resolveHandle`, `getRecord` → avatar cid/mime, `getBlob`). No PDS auth needed. - `lib/gravatar.ts` — `GravatarClient`: `list` / `upload` / `activate` / `delete` / `cleanup` + `emailHash` (SHA-256 of normalised email). Bearer-token auth. - `lib/state.ts` — `StateStore`: persists the last-synced cid on the mounted volume. - `main.ts` — `loadConfig` (env, injectable for tests), `sync()` orchestration, `main()` entry. Each run resolves handle → DID, reads the avatar blob ref, **skips if the content-addressed cid is unchanged**, else fetches the blob → uploads → activates → best-effort prunes stale images → persists the cid. The freshly uploaded image is protected by identity, so a flaky `selected` flag can never delete it. **Tests (`main_test.ts`)** — 7 tests, all using an in-process fake `fetch` (no real network): - fresh upload + activate - skip on unchanged avatar - re-upload + prune previous on change - graceful no-op when avatar removed - cleanup never deletes the new image even with a flaky `selected` flag - `loadConfig` missing-vars rejection + defaults/trailing-slash trimming **Packaging & ops** - `Containerfile` — multi-stage `deno compile` → standalone binary on `debian:stable-slim` (+ ca-certificates). - `compose.yml` — optional local/manual runs. - `systemd/gravatar-avatar-sync.{service,timer}` — oneshot `podman run --rm`, daily + `Persistent=true`. - `.env.example`, `.gitignore`. - `.forgejo/workflows/ci.yml` — `fmt --check` / `lint` / `check` / `test` + a container build job (the existing `pi.yml` is untouched). - `README.md` — what it does, env-var table, how to run (podman / compose / systemd), token generation. ## Notes - **Zero deps by design** — only Web-platform builtins; no external imports, so there is no `deno.lock` to commit. - Validated locally: `deno fmt --check`, `deno lint`, `deno check main.ts` clean; `deno test` → 7 passed; `deno compile` produces a working binary (exits 1 with a clear message on missing env). The systemd units parse cleanly. The container build couldn't run inside this sandbox (buildkit needs `catatonit`), but the `deno compile` step it performs was verified directly. - Per the issue, scheduling/deployment wiring lives elsewhere — these are the template units.
feat: self-contained Deno app mirroring PDS avatar → Gravatar
Some checks failed
CI / lint-test (pull_request) Failing after 3s
CI / container (pull_request) Failing after 9s
8d87de47c1
Co-authored-by: alex <alex@users.noreply.github.com>
up
Some checks failed
CI / lint-test (pull_request) Failing after 4s
CI / container (pull_request) Failing after 9s
a992c4dc41
fix ci
Some checks failed
CI / lint-test (pull_request) Failing after 21s
64ead778dc
Owner

/pi fix the type errors, see https://forge.l3x.in/alex/atproto-gravatar-sync/actions/runs/6/jobs/0/attempt/1#jobstep-5-1
Push changes back into the PR

/pi fix the type errors, see https://forge.l3x.in/alex/atproto-gravatar-sync/actions/runs/6/jobs/0/attempt/1#jobstep-5-1 Push changes back into the PR
TypeScript 5.7 made Uint8Array generic over its buffer type, so a bare
`Uint8Array` is now `Uint8Array<ArrayBufferLike>` — which is not assignable
to `BlobPart`/`BodyInit` (they require `Uint8Array<ArrayBuffer>`). This broke
`deno check` on the latest Deno used by CI at two sites.

Coerce via `.slice()`, which returns `Uint8Array<ArrayBuffer>` and compiles
cleanly on both old and new Deno (unlike the `Uint8Array<ArrayBuffer>`
annotation, which errors as non-generic on TS <5.7).

Co-authored-by: alex <alex@users.noreply.github.com>
Author
Contributor

Now let me look at the test file and then reproduce the type error locally.Let me get the CI status with the correct repo context and fetch the failing logs.The API isn't returning the logs. Let me install Deno and reproduce the type error locally.The check passed with Deno 2.1.4, but CI uses the latest version via setup-deno@v2. Let me try with the latest version to reproduce.I reproduced the type error. This is the well-known TypeScript 5.7+ change where Uint8Array is now generic over its buffer type (Uint8Array<ArrayBufferLike>), but BlobPart requires Uint8Array<ArrayBuffer>. Let me check the full extent of the type errors across the project.I've reproduced both type errors. This is the TypeScript 5.7+ change where Uint8Array became generic over its buffer type (Uint8Array<ArrayBufferLike>), but Blob/Response body types require Uint8Array<ArrayBuffer>. There are two error sites:

  1. lib/gravatar.ts:62new Blob([bytes], ...)
  2. main_test.ts:93new Response(state.avatar.bytes, ...)

Let me check the config and producers of the byte arrays to choose the right fix.Let me verify the cross-version compatibility of different fix approaches. First, check the TS versions and test the generic-typing approach on older Deno.Confirmed: the generic Uint8Array<ArrayBuffer> syntax breaks older Deno (TS2315). Let me test a cross-version-safe coercion approach.I have the full picture now. The errors stem from TypeScript 5.7+ making Uint8Array generic (Uint8Array<ArrayBufferLike>), which isn't assignable to BlobPart/BodyInit (they need Uint8Array<ArrayBuffer>). I confirmed the generic-syntax fix (Uint8Array<ArrayBuffer>) breaks older Deno (TS2315), so I'll use a cross-version-safe coercion via .slice(), which returns Uint8Array<ArrayBuffer> and compiles on all Deno versions.

Let me apply the fix to both sites.Now let me verify the fix compiles and tests pass on all three Deno versions (old, mid, latest/CI).All green across all three Deno versions (2.1.4, 2.4.0, and 2.9.2 — the latest that CI's setup-deno@v2 installs). The previously-failing deno check now passes, fmt/lint clean, and all 7 tests pass. Let me review the final diff.The fix is complete and verified. Now let me push the changes back into the PR.Fixed the type errors reported by CI.

Root cause: TypeScript 5.7 (shipped in Deno 2.2+) made Uint8Array generic over its buffer type, so a bare Uint8Array is now inferred as Uint8Array<ArrayBufferLike>. That isn't assignable to BlobPart/BodyInit, which require Uint8Array<ArrayBuffer>. CI's setup-deno@v2 pulls the latest Deno (2.9.2 / TS 6.0), so it caught two errors that older Deno did not:

  • lib/gravatar.ts:62new Blob([bytes], ...) in upload()
  • main_test.ts:93new Response(state.avatar.bytes, ...) in the fake getBlob

Fix: Coerce with .slice(), which returns a Uint8Array<ArrayBuffer> (copying into a real ArrayBuffer). I deliberately avoided the Uint8Array<ArrayBuffer> type annotation because it errors as "Type 'Uint8Array' is not generic" (TS2315) on Deno <2.2, whereas .slice() compiles cleanly everywhere.

Verification (all three against Deno 2.1.4, 2.4.0, and 2.9.2/CI):

  • deno fmt --check
  • deno lint
  • deno check main.ts main_test.ts (previously failed on 2.9.2)
  • deno test → 7 passed

Changes pushed to the PR.


View action run | Model: zai/glm-5.2 (thinking: xhigh) | Time: 6m 54s | Tokens: 328.3K | Pi SDK v0.80.3 | Action v2.25.1-develop (develop @ 5c04899)

Now let me look at the test file and then reproduce the type error locally.Let me get the CI status with the correct repo context and fetch the failing logs.The API isn't returning the logs. Let me install Deno and reproduce the type error locally.The check passed with Deno 2.1.4, but CI uses the latest version via `setup-deno@v2`. Let me try with the latest version to reproduce.I reproduced the type error. This is the well-known TypeScript 5.7+ change where `Uint8Array` is now generic over its buffer type (`Uint8Array<ArrayBufferLike>`), but `BlobPart` requires `Uint8Array<ArrayBuffer>`. Let me check the full extent of the type errors across the project.I've reproduced both type errors. This is the TypeScript 5.7+ change where `Uint8Array` became generic over its buffer type (`Uint8Array<ArrayBufferLike>`), but `Blob`/`Response` body types require `Uint8Array<ArrayBuffer>`. There are two error sites: 1. `lib/gravatar.ts:62` — `new Blob([bytes], ...)` 2. `main_test.ts:93` — `new Response(state.avatar.bytes, ...)` Let me check the config and producers of the byte arrays to choose the right fix.Let me verify the cross-version compatibility of different fix approaches. First, check the TS versions and test the generic-typing approach on older Deno.Confirmed: the generic `Uint8Array<ArrayBuffer>` syntax breaks older Deno (TS2315). Let me test a cross-version-safe coercion approach.I have the full picture now. The errors stem from TypeScript 5.7+ making `Uint8Array` generic (`Uint8Array<ArrayBufferLike>`), which isn't assignable to `BlobPart`/`BodyInit` (they need `Uint8Array<ArrayBuffer>`). I confirmed the generic-syntax fix (`Uint8Array<ArrayBuffer>`) breaks older Deno (TS2315), so I'll use a cross-version-safe coercion via `.slice()`, which returns `Uint8Array<ArrayBuffer>` and compiles on all Deno versions. Let me apply the fix to both sites.Now let me verify the fix compiles and tests pass on all three Deno versions (old, mid, latest/CI).All green across all three Deno versions (2.1.4, 2.4.0, and 2.9.2 — the latest that CI's `setup-deno@v2` installs). The previously-failing `deno check` now passes, fmt/lint clean, and all 7 tests pass. Let me review the final diff.The fix is complete and verified. Now let me push the changes back into the PR.Fixed the type errors reported by CI. **Root cause:** TypeScript 5.7 (shipped in Deno 2.2+) made `Uint8Array` generic over its buffer type, so a bare `Uint8Array` is now inferred as `Uint8Array<ArrayBufferLike>`. That isn't assignable to `BlobPart`/`BodyInit`, which require `Uint8Array<ArrayBuffer>`. CI's `setup-deno@v2` pulls the latest Deno (2.9.2 / TS 6.0), so it caught two errors that older Deno did not: - `lib/gravatar.ts:62` — `new Blob([bytes], ...)` in `upload()` - `main_test.ts:93` — `new Response(state.avatar.bytes, ...)` in the fake `getBlob` **Fix:** Coerce with `.slice()`, which returns a `Uint8Array<ArrayBuffer>` (copying into a real `ArrayBuffer`). I deliberately avoided the `Uint8Array<ArrayBuffer>` type annotation because it errors as "Type 'Uint8Array' is not generic" (TS2315) on Deno <2.2, whereas `.slice()` compiles cleanly everywhere. **Verification** (all three against Deno 2.1.4, 2.4.0, and 2.9.2/CI): - `deno fmt --check` ✅ - `deno lint` ✅ - `deno check main.ts main_test.ts` ✅ (previously failed on 2.9.2) - `deno test` → 7 passed ✅ Changes pushed to the PR. --- [View action run](https://forge.l3x.in/alex/atproto-gravatar-sync/actions/runs/7) | Model: zai/glm-5.2 (thinking: xhigh) | Time: 6m 54s | Tokens: 328.3K | Pi SDK v0.80.3 | Action v2.25.1-develop (develop @ 5c04899)
wip
Some checks failed
CI / lint-test (pull_request) Failing after 23s
31ccb56be9
wip
Some checks failed
CI / lint-test (pull_request) Failing after 22s
3bad300866
wip
Some checks failed
CI / lint-test (pull_request) Failing after 8s
53c11d51ef
wip
Some checks failed
Build and Push Container Image / build-and-push (pull_request) Failing after 23s
4db2479c4c
wip
Some checks failed
Build and Push Container Image / build-and-push (pull_request) Failing after 21s
c753a30dbc
wip
All checks were successful
Build and Push Container Image / build-and-push (pull_request) Successful in 55s
9b1e1c0481
alex merged commit b0f68f6761 into master 2026-07-10 11:47:02 +00:00
alex deleted branch pi/issue1-1783629626256 2026-07-10 11:47:03 +00:00
Sign in to join this conversation.
No reviewers
No labels
dependencies
No milestone
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
alex/atproto-gravatar-sync!2
No description provided.