Skip to content

Releases: obakeng-develops/campsend

v0.13.0

Choose a tag to compare

@obakeng-develops obakeng-develops released this 04 Sep 11:05
bbd4746

No application code changed since 0.12.0. Cut so the managed distribution can release the plans it added, which asserts both distributions carry the same version.

v0.12.0

Choose a tag to compare

@obakeng-develops obakeng-develops released this 04 Sep 09:58
27c28ac

Sign-in can now bring you back to where you started.

Sign-in return

A visitor who clicked something, signed in, and landed on their files had been dropped: whatever they were part way through was gone. Sign-in now takes an optional return_to, and after the emailed link is consumed that is where they land. The existing intent=send behaviour still decides when there is no return_to.

It rides on the login token rather than the session, because sign-in resets the session and the link is often opened on a different device from the one that asked for it.

Only a path on this site is accepted, checked where it enters and again before it is stored, so it cannot become an open redirect. Anything else is dropped and the visitor lands on their files.

after_sign_in_path(intent:, return_to:) on the Authentication concern is the single answer to where somebody lands, used by both the sign-in form and the emailed link.

Also since 0.11.1

The npm advisories audit no longer fails the build when the registry cannot answer. bin/importmap-audit tolerates every Importmap::Npm::HTTPError and still fails on a real advisory.

Migrations

One, adding a nullable return_to column to login_tokens.

v0.11.3

Choose a tag to compare

@obakeng-develops obakeng-develops released this 04 Sep 08:58
6752af4

Tolerates every way the npm advisories endpoint fails, not just the slow one.

0.11.2 tolerated a hang past importmap`s read timeout by matching one error message. The endpoint then began answering fast with a 503 instead, which is the same condition in different words, so builds went red again.

bin/importmap-audit now tests for Importmap::Npm::HTTPError. That class is raised at every point where the registry fails to answer, and never by a real advisory, which prints a table and exits without raising.

No application code changed in this release.

v0.11.2

Choose a tag to compare

@obakeng-develops obakeng-develops released this 04 Sep 08:39
f25c23e

Stops a slow npm registry failing the whole build.

bin/importmap audit asks the npm registry which pinned packages have advisories against them. That endpoint answers either side of the sixty second read timeout importmap gives it, so about half of all CI runs failed on it — on diffs containing no JavaScript at all.

A transport failure is now reported and tolerated. Anything else the audit says still fails the build. The tolerance lives in bin/importmap-audit, which both bin/ci and the scan_js workflow job call, so the two cannot disagree about what a slow registry means.

No application code changed in this release.

v0.11.1

Choose a tag to compare

@obakeng-develops obakeng-develops released this 03 Sep 13:10
432e739

Two error messages name the scope the way the tokens page does.

Using a read token to send answered This token can only read. Create a token with the write scope. The tokens page offers "Read deliveries" and "Read and send deliveries", so "write" is a word that appears nowhere a person can see. Both the REST 403 and MCP's create_delivery now say Create one that can read and send.

write is unchanged as the stored value and the scope check.

Full changelog: v0.11.0...v0.11.1

v0.11.0

Choose a tag to compare

@obakeng-develops obakeng-develops released this 03 Sep 12:24
e5cc45e

Campsend has an HTTP API.

Deliveries over plain HTTP

GET /api/v1/deliveries, POST /api/v1/deliveries and GET /api/v1/deliveries/:identifier, authenticated with an API token as a bearer token. Create, list and read were already there over MCP, but MCP is JSON-RPC, which a shell script or an automation tool cannot speak.

Read and write scopes are enforced. A delivery belonging to somebody else answers 404 rather than 403, so nobody can probe which identifiers exist. No response carries an access token, a token digest or a signed storage URL.

Direct uploads take a token

This is the half that makes the rest usable. Api::V1::DirectUploadsController read the session, so a token holder could not upload anything and could only send files a browser had already put there.

It now accepts a bearer token as well as a session, and requires the write scope when one is used. So a script can reserve an upload, put the bytes at the URL it is given and send the delivery, with no browser anywhere.

CSRF verification is skipped only for token callers. A bearer token is not a cookie, so there is nothing for a forged request to ride on, and a browser session still carries one and still gets checked.

Full changelog: v0.10.0...v0.11.0

v0.10.0

Choose a tag to compare

@obakeng-develops obakeng-develops released this 02 Sep 12:21
082ba29

Two seams for whoever runs Campsend as a product.

Wide events can go somewhere that survives a deploy

config.x.wide_event_sink takes a callable and receives a copy of every event, after the log line is written. Nil here, so a self-hosted install still just logs.

The reason: wide events go to stdout, the container keeps them, and a deploy throws them away. A user signed up, signed in, uploaded nothing and left. Their events would have shown where they stopped, and by the time anyone looked a deploy had happened and the oldest surviving line was 50 minutes after they had gone.

If you write one: it receives the payload with symbol keys, not the JSON the log line carries, and it is called inline on the request thread. Hand off to a queue or a thread rather than making a network call in it.

The policy can write the storage sentence

storage_usage_for may return a :note, which replaces "Sent files stay in My Files." after the figures in the composer. The default is unchanged and is accurate here, where nothing removes a file you kept. A distribution whose files expire can say so where somebody is deciding what to upload.

Full changelog: v0.9.0...v0.10.0

v0.9.0

Choose a tag to compare

@obakeng-develops obakeng-develops released this 02 Sep 11:40
a73eb67

One seam, so a distribution can add to the layout head without adding to core.

config.x.extension_head_partials

Empty here. A distribution appends a partial name and it renders in <head> after yield :head. It mirrors config.x.extension_stylesheets, which already worked this way.

The reason it exists: analytics. A snippet added to this layout would mean every self-hosted install reporting into somebody else's project, which is not something an MIT file-delivery app should do quietly. A stock install renders no third-party script and its content security policy is unchanged.

A correction

This release also removes extension_script_src and extension_connect_src, which were added and then taken out again before any release carried them.

They could not work. config.content_security_policy evaluates its block when the initializer loads, which is before any engine initializer has run, so the arrays were always empty when read and the policy never gained the origins.

If you build on Campsend and need to widen the policy, mutate it after the fact:

config.after_initialize do |app|
  policy = app.config.content_security_policy
  policy&.script_src(*policy.directives.fetch("script-src", []), "https://example.com")
end

Reading the existing directives back preserves whatever the base policy set.

Full changelog: v0.8.0...v0.9.0

v0.8.0

Choose a tag to compare

@obakeng-develops obakeng-develops released this 02 Sep 11:01
1d12b3e

Two seams, both for distributions that lift the defaults.

Multipart reaches per-user buckets

MultipartUpload.supported? asked a service for a client and a bucket. A service that resolves a different bucket per blob key has neither, so every upload it handled stayed on the single PUT path at roughly 5 GB. Nothing failed, which is what made it easy to miss.

Such a service can now answer multipart_service_for(key) and return the one bucket that key lives in. abort_abandoned! still takes a single bucket, so a service fronting many has to call it per bucket rather than passing itself.

The per-file limit is separate from the delivery total

max_send_size_for was read in two places that mean different things. Api::V1::DirectUploadsController, User#reserve_blob! and GoogleDriveImportJob check one file; DeliveryRevision and Collection check files.sum(&:byte_size). Before multipart both were bounded by the same physical wall, so conflating them cost nothing. Now that the wall is gone, one large file and twenty smaller ones summing to the same total are different things.

Campsend::Policy#max_file_size_for(user) returns max_send_size_for(user), so one file can still fill a delivery and nothing changes unless you override it.

Full changelog: v0.7.0...v0.8.0

v0.7.0

Choose a tag to compare

@obakeng-develops obakeng-develops released this 02 Sep 10:42
72707cd

A delivery can now be larger than one upload request.

Multipart direct upload

ActiveStorage::Service::S3Service#url_for_direct_upload issues exactly one presigned PUT, and S3 and R2 both refuse a single PUT above 4.995 GiB. That was the real ceiling on a delivery, on every plan and every self-hosted install, and no configuration moved it. multipart_upload_threshold looks like the fix and is not: it applies only to server-side uploads, which the direct upload path never takes.

A file over 100 MB now uploads in parts. The browser asks for a window of presigned part URLs, sends the parts itself, and tells the server when to assemble them. Whatever Campsend::Policy#max_send_size_for returns is now the only ceiling you have.

This needs an S3-compatible bucket. Disk storage still sends one request per file and still stops around 5 GB.

Your bucket needs one CORS line

"ExposeHeaders": ["ETag"] is no longer decorative. Each part upload returns an ETag that the browser has to read and send back, so without that rule an upload fails at the last step, after every byte has already gone over the wire. The self-hosting docs have carried this rule from the start; now it matters.

Abandoned uploads clean themselves up

An upload a browser starts and never finishes leaves its parts in the bucket, and no storage provider removes them on its own. SecurityCleanupJob now aborts anything still in flight after a day, before it purges the blob that reserved it. It is already scheduled daily, so there is nothing to add.

There is no new table. The upload id travels to the browser in a signed token and comes back on every call, and reconciliation asks the bucket what is in flight rather than keeping a second copy that can disagree with it.

Uploads that lie about their size are thrown away

A reservation trusts a client-supplied byte count. With one PUT that is self-limiting. With multipart it is not, so a client could reserve a megabyte and send ten gigabytes past the storage limit. Completion measures the assembled object and purges it if it does not match what was reserved.

The composer says what it means

It used to say "2 GB total", which reads as an account total rather than a per-delivery limit. It now says "per delivery", and a distribution that meters storage can report usage beside it through the new Campsend::Policy#storage_usage_for, which returns nil here.

Also

config/environments/development.rb reads ACTIVE_STORAGE_SERVICE the way production.rb already did, so you can point development at a real bucket.

Minor rather than patch: three new endpoints, a new model, and two new policy methods. Nothing changes for an install that only ever uploads small files.

Full changelog: v0.6.0...v0.7.0