117 Commits

Author SHA1 Message Date
Julien Calixte
7623fff227 fix(test): match getSession URL by suffix, not /getSession path
All checks were successful
CI / check (push) Successful in 13s
The PDS endpoint URL is /xrpc/com.atproto.server.getSession —
those are dots before getSession, not slashes, so the previous
includes("/getSession") filter never matched and the Authorization
header assertion silently short-circuited via optional chaining.
2026-06-07 22:31:26 +02:00
Julien Calixte
2105d75cb5 ci(gitea): scope deno test and check to the new tests directory
Some checks failed
CI / check (push) Failing after 11s
2026-06-07 22:27:23 +02:00
Julien Calixte
e937863bc6 test(server): cover HTTP routes, auth, admin gate, and CORS preflight
Some checks failed
CI / check (push) Failing after 12s
2026-06-07 22:26:54 +02:00
Julien Calixte
5c4b189136 test(jetstream): cover bulk-create debounce and flush semantics
Some checks failed
CI / check (push) Failing after 11s
Uses @std/testing/time FakeTime to deterministically advance the
500ms debounce. Covers accumulation, timer restart on each push,
the bulk-create verb + payload shape, and per-did buffer
isolation with cleared state after flush.
2026-06-07 22:25:48 +02:00
Julien Calixte
764ba09f77 test(jetstream): cover dispatchAll resilience and header handling
Some checks failed
CI / check (push) Failing after 13s
2026-06-07 22:24:51 +02:00
Julien Calixte
365deb305a test(search): cover cursor, snippet, and searchNotes query body
Some checks failed
CI / check (push) Failing after 10s
2026-06-07 22:24:21 +02:00
Julien Calixte
02f3139f46 refactor(search): read OPENSEARCH_* env per call
Some checks failed
CI / check (push) Failing after 12s
Tests can't override values captured at module load because ES
imports hoist before any Deno.env.set call. Reading env on each
helper invocation makes the module trivially testable and has
no production cost (env doesn't change at runtime).
2026-06-07 22:23:36 +02:00
Julien Calixte
6159ec00e0 ci(gitea): cache DENO_DIR keyed on deno.lock
Some checks failed
CI / check (push) Failing after 16s
setup-deno's built-in cache stores fetched JSR/npm modules and
compiled dependencies across runs. Keying on the lockfile hash
invalidates the cache only when dependencies change.
2026-06-07 22:22:10 +02:00
Julien Calixte
3611add21a test(auth): cover JWT decoding and authenticateRequest flow 2026-06-07 22:21:48 +02:00
Julien Calixte
34b8221376 fix(test): pass explicit did to seedWebhook in CRUD test
All checks were successful
CI / check (push) Successful in 15s
The webhook CRUD test combined an explicit did:plc:user webhook
with seedWebhook calls that defaulted to did:plc:test, so
listWebhooksByDid("did:plc:user") only returned the one
explicit row instead of three.
2026-06-07 22:20:52 +02:00
Julien Calixte
292fe8f85a test(migrations): cover initial schema, idempotency, and legacy upgrade
Some checks failed
CI / check (push) Failing after 17s
2026-06-07 22:17:54 +02:00
Julien Calixte
2d7863d9cc test(db): cover note CRUD, pagination, and webhook helpers 2026-06-07 22:17:23 +02:00
Julien Calixte
c62936b819 test(helpers): add db, fetch, app, and auth test helpers
withTestDb sets up an in-memory SQLite handle with migrations
applied, swaps the production singleton via _setDbForTest, and
restores in a finally block. installFetchStub captures every
outbound request and throws if restored twice. requestApp wraps
Oak's app.handle so route tests can run without listen().
stubAuthenticate flips the server's authenticator indirection.
2026-06-07 22:16:47 +02:00
Julien Calixte
62a55fd3e4 fix(jetstream): use ReturnType<typeof setTimeout> for bulk timer
All checks were successful
CI / check (push) Successful in 14s
setTimeout returns an opaque Timeout type in newer Deno
releases, so the previous timer: number annotation fails type
checking in CI. Using ReturnType<typeof setTimeout> is portable
across Deno versions and runtimes.
2026-06-07 22:14:03 +02:00
Julien Calixte
49608cff20 refactor(search,auth): export pure helpers for direct testing
encodeCursor/decodeCursor/firstSnippet (opensearch) and
decodeJwtPayload (verify) become public exports so tests can
hit them without round-tripping through fetch.
2026-06-07 22:06:49 +02:00
Julien Calixte
01048c753e refactor(jetstream): extract webhook dispatch and bulk-create modules
dispatchAll, fireWebhooks, and the bulk-create debounce buffer
move out of jetstream.ts into src/jetstream/{webhooks,bulk}.ts.
fireWebhooks and the bulk helpers now take their DB lookup and
dispatch as parameters so tests can inject stubs. Top-level
Jetstream construction, event handlers, ensureIndex call, and
the cursor-saving interval are gated behind import.meta.main so
importing jetstream.ts no longer opens a WebSocket.
2026-06-07 22:06:19 +02:00
Julien Calixte
8964f66184 refactor(server): extract createApp and add auth/admin test seams
createApp() builds the Application without listening, so tests
can call app.handle() directly. The listen call is gated behind
import.meta.main so importing server.ts no longer boots a port.
ADMIN_DIDS is read inside requireAdmin on each call (was frozen
at module load), and a mutable authenticator indirection lets
tests stub authenticateRequest via _setAuthenticatorForTest.
2026-06-07 22:05:12 +02:00
Julien Calixte
6760c434b4 refactor(db): lazy-init connection and add test seam
The Database handle is now constructed on first use and held in
a module-level slot. New _setDbForTest and _resetDbForTest
exports let tests swap in a :memory: handle without rewriting
callers. closeDb() replaces the previous db.close() pattern used
by short-lived scripts. Adds listDistinctNoteDids() to keep the
backfill script from poking the raw connection.
2026-06-07 22:04:12 +02:00
Julien Calixte
dc6daf3f54 refactor(migrations): extract applyMigrations as callable function 2026-06-07 22:01:57 +02:00
Julien Calixte
7875d24d48 refactor(search): use typed OpenSearch SDK request body
Some checks failed
CI / check (push) Failing after 1m0s
Pull in @opensearch-project/opensearch only for its
Search_RequestBody type so the inline search body is
type-checked. Also drops the unsupported
highlight.content.boundary_scanner_locale option.
2026-06-07 12:04:43 +02:00
Julien Calixte
4ff3ea6644 docs(api): add Bruno collection for Jetstream API
Excludes docs/ from deno fmt and deno lint so the Bruno
files don't break formatting and lint checks in CI.
2026-06-07 12:04:37 +02:00
Julien Calixte
a6f1a96c4a ci(gitea): migrate pipeline from GitLab to Gitea Actions 2026-06-07 12:04:25 +02:00
Julien Calixte
77f24bf7ce chore(docker): include backfill script in image
Backfill must read the SQLite volume that lives inside the api/jetstream
containers, so it has to be runnable via `deno task backfill:search`
from a Coolify exec shell.
2026-06-07 11:36:11 +02:00
Julien Calixte
d2248424a7 docs(api): add OpenAPI 3.1 spec for the HTTP server 2026-06-07 11:29:02 +02:00
Julien Calixte
1a3461611d docs(readme): document search endpoints and backfill 2026-06-07 11:26:29 +02:00
Julien Calixte
59ff150717 chore(docker): add OpenSearch service to compose
Single-node cluster with the security plugin disabled (in-cluster HTTP
only). jetstream and api now depend on it being healthy and receive
OPENSEARCH_URL via env.
2026-06-07 11:26:25 +02:00
Julien Calixte
d7e189c61f feat(scripts): add OpenSearch backfill task
Iterates distinct DIDs in SQLite, resolves each one's PDS, and reindexes
every space.remanso.note record into OpenSearch via
com.atproto.repo.listRecords. Needed because note content is not
persisted to SQLite, so the index would otherwise only cover notes
created after deployment.
2026-06-07 11:26:20 +02:00
Julien Calixte
e7796b47c3 feat(server): add /search and /:did/search endpoints
GET /search is public and filters to discoverable=true. GET /:did/search
requires a JWT for that DID and includes non-discoverable notes (drafts).
Both reuse the existing PAGINATION default and { results, cursor }
envelope. The requireDidOwnership 403 message is generalized since it
now guards more than webhooks.
2026-06-07 11:26:14 +02:00
Julien Calixte
937e16e602 feat(jetstream): index notes in OpenSearch on write
Wires create/update/delete events to indexNote/removeNote alongside the
existing webhook dispatch via Promise.allSettled, so search-index
failures cannot break webhook delivery. Calls ensureIndex once at
startup behind a try/catch so a transient OpenSearch outage does not
crash the listener.
2026-06-07 11:26:08 +02:00
Julien Calixte
45db68c34d feat(search): add OpenSearch client module
Exposes ensureIndex, indexNote, removeNote, and searchNotes. searchNotes
runs a fuzzy multi_match across title+content, returns sentence-bounded
highlights, and paginates via base64-encoded search_after cursors. All
operations no-op when OPENSEARCH_URL is unset so callers stay safe.
2026-06-07 11:26:02 +02:00
Julien Calixte
db1e07060c feat(note): add optional content field to Note type 2026-06-07 11:25:44 +02:00
Julien Calixte
6163092d27 refactor(auth): export resolvePds for reuse 2026-06-07 11:25:39 +02:00
Julien Calixte
6f4d8d3b56 chore: add dockerignore for scripts, local artifacts, and env files
The image previously inherited everything from a `COPY . .`, including
.env (secrets), local notes.db copies, and admin scripts that should
not run in prod containers.
2026-05-05 15:04:43 +02:00
Julien Calixte
355fc45316 refactor(scripts): switch webhooks:all to api fetch
Hits GET /admin/webhooks instead of opening the local SQLite directly,
so the task can be run from a developer laptop without ssh or file
access to the server. Drops the FFI/read/write task permissions in
favour of net/env.
2026-05-05 14:07:31 +02:00
Julien Calixte
1c160b6c53 refactor(scripts): extract atproto session helpers to shared module 2026-05-05 14:07:26 +02:00
Julien Calixte
34faa10be2 feat(webhooks): add admin endpoint to list every subscription
Adds GET /admin/webhooks gated by an ADMIN_DIDS env-var allowlist of
verified AT Proto DIDs. Fail-closed: if ADMIN_DIDS is unset, the route
always returns 403 — no accidental exposure on deploys that forget it.
2026-05-05 14:07:22 +02:00
Julien Calixte
c00f3d631c chore(scripts): add admin task to list every webhook
Direct SQLite read with no DID filter — complements the API-backed
`webhooks list`, which is scoped per DID.
2026-05-05 14:00:10 +02:00
Julien Calixte
911d062423 chore(jetstream): log bulk-create record count on flush 2026-05-05 13:46:00 +02:00
Julien Calixte
8055060af3 fix: increase create debounce time 2026-05-05 12:48:15 +02:00
Julien Calixte
5cb581123d chore(scripts): add list and delete commands to manage-webhooks
`deno task webhooks list` prints the authenticated DID's subscriptions
(id, url, method, verb). `deno task webhooks delete --id <id>` removes
a single one — pair with `list` to pick which to drop instead of
nuking everything via delete-all.
2026-05-05 12:38:32 +02:00
Julien Calixte
bcea56c529 feat(webhooks): add list and granular delete endpoints
- GET /:did/webhooks lists subscriptions for the authenticated owner
  (token field excluded — write-only as elsewhere).
- DELETE /:did/webhooks/:id deletes a single subscription. The query
  scopes on (did, id) so a verified caller cannot delete rows that
  belong to a different DID even with a valid id.

Also extracts the auth gate into requireDidOwnership now that three
endpoints share it.
2026-05-05 12:38:26 +02:00
Julien Calixte
a3c92254ea refactor(scripts): auto-resolve PDS from handle, drop BSKY_PDS
Inputs are now just ATPROTO_HANDLE, ATPROTO_APP_PASSWORD, REMANSO_API
(or matching --handle, --app-password, --api). The script resolves the
handle to a DID via public.api.bsky.app, then resolves the DID to a
PDS via plc.directory — same path the server's verifier uses. Works
for any AT Protocol PDS (Bluesky, eurosky, self-hosted) without the
caller having to know the PDS URL.
2026-05-05 12:36:22 +02:00
Julien Calixte
e0fe4ce16f chore(scripts): add deno task webhooks for register and delete-all
Wraps the createSession + Authorization: Bearer flow so callers don't
need to assemble curl by hand. Reads BSKY_IDENTIFIER / BSKY_PASSWORD /
BSKY_PDS / REMANSO_API from env (or matching flags). Defaults to
bsky.social so non-bsky-hosted accounts must set BSKY_PDS explicitly,
e.g. BSKY_PDS=https://eurosky.social.
2026-05-05 12:32:43 +02:00
Julien Calixte
1ce5d9150d feat(auth): require DID owner JWT to manage webhooks
Both POST /:did/webhooks and DELETE /:did/webhooks were unauthenticated:
anyone could register a webhook for someone else's DID (privacy leak)
or wipe a DID's webhook list (DoS on legitimate subscribers). Now both
endpoints require a Bluesky session bearer token, verified end-to-end
against the DID's PDS via the existing authenticateRequest helper, and
the verified DID must match the URL :did.
2026-05-05 12:27:22 +02:00
Julien Calixte
7b53909c52 feat(webhooks): add per-verb subscriptions and bulk-create debounce
Subscriptions now declare a `verb` (create | delete | bulk-create).
POST /:did/webhooks defaults to inserting both create and delete rows
when no verb is given, preserving existing all-events behavior. Update
events fold into the create verb. The new bulk-create verb debounces
creates per DID over 400 ms and delivers a `records` array.

Migration adds the verb column with default 'create' and clones every
existing row for the delete verb so legacy subscriptions keep firing
on both events.
2026-05-05 12:25:54 +02:00
Julien Calixte
8e967ba43c prettier 2026-03-21 12:36:33 +01:00
Julien Calixte
555bffaa89 fix: disable restart on migrate service 2026-03-21 12:33:07 +01:00
Julien Calixte
ac2b70a260 fix: prevent database is locked on concurrent startup
Set busy_timeout before journal_mode=WAL in db.ts so SQLite retries
for 10s instead of failing immediately with the default 0ms timeout.

Extract migration into a dedicated Docker Compose service so both
jetstream and api wait for it to complete before opening the database.
2026-03-21 12:30:17 +01:00
Julien Calixte
92e0dbe0d4 feat: add GitHub OAuth proxy endpoint at /auth/github 2026-03-21 10:35:49 +01:00
Julien Calixte
32ee135099 fix: install curl for Docker healthcheck 2026-03-17 02:27:21 +01:00