Skip to content

Library staff attribution, restricted runtime, and CDN delivery - #58

Merged
sawradip merged 14 commits into
mainfrom
feat/library-staff-delivery
Nov 13, 2026
Merged

sawradip merged 14 commits into
mainfrom
feat/library-staff-delivery

Conversation

@sawradip

Copy link
Copy Markdown
Contributor

Lets trusted machine keys attribute library changes to staff or background jobs through an explicit act-for capability. Reserved staff/system identities cannot be supplied by ordinary callers; malformed acting requests fail without changing data. Human accounts can be disabled, and deployments can run migrations separately from restricted runtime connections.

Public libraries can redirect suitable files to an explicitly configured CDN. Own-server/default URLs retain pre-signed delivery, SVG retains attachment handling, and browser cache lifetimes and withdrawal/purge behavior are documented.

Validation completed during the November 11 review:

  • Full pnpm check passed in both source branches.
  • Corrected attribution branch: 114 library tests passed; its optional Postgres case was covered separately.
  • 17 real Postgres 18 runtime/attribution tests passed.
  • Combined library/media suite: 117 passed, with the optional Postgres case covered separately.

Includes CI coverage for restricted Postgres runtime credentials. Configure writing keys with act-for before upgrading dashboard/job traffic. CDN rules, runtime credentials, and optional human-login retirement remain operator-controlled release steps; no production migration is executed here.

Companion PRs:

sawradip and others added 14 commits November 11, 2026 07:13
…C_URL, kept a week

On S3 a public library answered /v1/files/{key} with a 302 to a 5-minute
pre-signed URL on the bucket's raw endpoint (private, max-age=240), so apps
linking one file by key paid an extra round trip every few minutes and
fetched uncached from R2. It now sends them to the file under PUBLIC_URL,
where /v1/stores already names it and a CDN keeps it: public, max-age=604800
for the standard path, 300 s for a file adopted in place. A private library
still pre-signs; so does a public one whose PUBLIC_URL is its own /files/.
Withdrawn keys stay 410. Spec 1.2 registry API §7 says it is allowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…COUNTS=off

Staff of a deployment can sign in to their own app instead of the library: the
app's server calls with one key and says who it acts for, and the library's
records name that person.

- auth_api_keys.act_for (migration step 7, additive) and an actors table
  (id, name, first_seen_at, last_seen_at); actFor on key records, settable on
  create (POST v1/admin/keys, key add --act-for) and by PATCH v1/admin/keys/{id}
  (key act-for <id> on|off).
- Chalk-Actor: base64url JSON {id, name}. From an act-for key the caller's
  who-value becomes the id, so uploads, adopted files, highlight reviews and
  drawn boxes (the body's `by` is replaced) and ?mine=1 all mean the person;
  actors are upserted at most once a minute per id and process. Any other
  caller gets 403 act_for_not_allowed, a malformed header 400. CORS allows it.
- GET v1/auth/me answers actFor and actingFor; GET v1/admin/actors lists actors.
- HUMAN_ACCOUNTS=off: password and refresh grants are unsupported_grant_type
  (and not in discovery), user admin endpoints 404, users' tokens refused, the
  user CLI commands refuse; keys and TOKENS keep working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deployment can run the server as a login with row rights only: the schema's
owner runs `chalk-library migrate` as a release step, and the server (and the
key/user CLI) then creates nothing at start and refuses a schema that lacks a
migration step it knows, with a message saying what to run. Default stays true.

CREATE SCHEMA is now issued only when the schema is missing: Postgres checks the
right to create schemas before IF NOT EXISTS, which an owner given its schema by
the setup does not have.

The runtime-login test makes its own database on a real Postgres
(CHALK_LIBRARY_TEST_DB): owner migrates, a DML-only login serves keys, act-for
uploads, trigram search, highlights and users.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ser accounts

Registry API §10.7 (Chalk-Actor: validation, recording the actor id, 403 for any
other caller) and §10.8 (no password or refresh grants, no user endpoints), with
actFor, PATCH keys and GET v1/admin/actors in §10.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntime logins

The settings, the migrate release step, the grants for a DML-only runtime login
and where pg_trgm goes (the setup creates it in the library's schema).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n own /files/, never for SVG

- The redirect to PUBLIC_URL is kept public, max-age=86400 (was a week), so a
  withdrawal reaches browsers within a day; adopted files stay 300 s.
- Whether PUBLIC_URL is a public domain is decided from the address: a
  /files/ path (the default when PUBLIC_URL is unset, even without
  SERVER_URL, which redirected to http://localhost:<port>/files/) pre-signs.
- An SVG is pre-signed as an attachment (needsAttachment in storage.ts),
  never sent to a public address that would open it inline.
- Tests: libraryFromEnv with PUBLIC_URL and SERVER_URL unset and with an own
  https://host/files/, the SVG case, an adopted path that needs encoding, and
  the public library's redirect while an adopted file moves.
- Spec §7/§8: how long a withdrawal takes to reach readers, and the CDN purge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ptional sandbox headers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s only after a change

From review of the act-for work:
- a caller not acting for someone may not send a highlight `by` starting with
  staff: or system: (400 actor_requires_act_for), so a plain author key or a
  bootstrap token cannot sign as a person or a job;
- import keeps the restored links' reviewers, but staff:/system: ones only from
  an act-for key or an administrator;
- a bodiless request (DELETE links/{id}) under Chalk-Actor counts as an empty
  body, so `by` comes from the header instead of failing with 400;
- actor names are trimmed and refuse Cc controls (C1 included), bidi
  embeddings, overrides and isolates, and U+2028/2029;
- the actor is recorded only after a 2xx answer to a non-GET/HEAD request:
  reads, refusals and failures leave no row;
- Chalk-Actor is no longer allowed by CORS: an act-for key belongs on a server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… after a change

Registry API §10.7 and §11.3: actor_requires_act_for for callers not acting for
someone, import keeping original reviewers (act-for key or admin only), names
without control or bidi characters, actors recorded after a successful change,
and act-for keys never used from a browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sign in elsewhere

A table of the keys (dashboard and library-sync with --act-for, CI and readers
without), the settings and the release step; writers that name a person or a
job need an act-for key and Chalk-Actor instead of a staff:/system: `by`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
migrate.test.ts (an owner migrates, a DML-only login serves with
MIGRATE_ON_START=false) skipped without CHALK_LIBRARY_TEST_DB, so it never ran
on a PR. The service moves to postgres:18; the test makes its own database and
logins on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only a genuinely empty DELETE may omit JSON. Invalid PATCH, DELETE and bulk removal bodies fail without changing links.

Co-Authored-By: Codex <noreply@openai.com>
@sawradip
sawradip merged commit 461fd82 into main Nov 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant