Skip to content

feat: paste links and removed-paste lifecycle in the API - #71

Merged
darwin67 merged 3 commits into
mainfrom
feat/api-paste-lifecycle
Oct 11, 2026
Merged

darwin67 merged 3 commits into
mainfrom
feat/api-paste-lifecycle

Conversation

@darwin67

Copy link
Copy Markdown
Member

Summary

The first of two stacked PRs that bring the API and CLI up to the redesigned paste model. This one is server-side only. The next PR adds the client and CLI on top of it.

  • Links in every response. Each paste carries url, the browser link people share, and raw_url, the exact stored bytes. Both are absolute. This covers the "API create/show responses contain canonical and raw URLs" item in RFD 3.
  • Removed pastes as a collection. The same three routes exist for the personal workspace and under /api/v1/workspaces/:workspace_id:
    • GET /api/v1/pastes/deleted lists restorable deleted and expired pastes. Each entry includes removal_reason, removed_at and purge_after, but never the content.
    • POST /api/v1/pastes/deleted/:id/restore restores a paste and returns its metadata.
    • DELETE /api/v1/pastes/deleted/:id deletes a paste permanently, before its recovery window ends.
  • Same rules as the Deleted tab. Only people who manage a paste can restore it or delete it permanently. An unknown id, another member's paste and an expired recovery window all return 404, so the API never confirms that a paste exists.
  • Route order. The deleted routes come before the /:id routes, so "deleted" is never read as a paste id.

Test plan

  • mix precommit: 563 tests, 0 failures
  • mix credo: no issues
  • New tests cover:
    • share and raw URLs on show and create
    • listing with reason and purge date, and content left out
    • restore and permanent delete
    • 404 for active, unknown and other people's pastes
    • workspace-scoped routes

🤖 Generated with Claude Code

https://claude.ai/code/session_019fnjMN7WpEjQJD2zveakV6


Generated by Claude Code

Every paste in the API now carries url, the browser link people share, and
raw_url, the exact stored bytes, as absolute URLs. The CLI and scripts can
print or open them without rebuilding paths from the server address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fnjMN7WpEjQJD2zveakV6
Removed pastes are their own collection, for the personal workspace and
for any joined workspace:

  GET    /api/v1/pastes/deleted
  POST   /api/v1/pastes/deleted/:id/restore
  DELETE /api/v1/pastes/deleted/:id

plus the same under /api/v1/workspaces/:workspace_id. Listings carry the
removal reason, when it happened and when the paste is purged, never the
content. Restore and permanent delete follow the same rules as the Deleted
tab, and answer 404 alike for an unknown id, another member's paste and an
expired recovery window, so they never confirm a paste exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fnjMN7WpEjQJD2zveakV6
@github-actions github-actions Bot added the feat label Oct 10, 2026
@darwin67
darwin67 added this pull request to stack #73 October 11, 2026 00:02
@darwin67
darwin67 marked this pull request as ready for review October 11, 2026 00:02
Include reference_url in the content-free removed-paste projection. Extend the API regression to assert the stored reference is returned while all content encoding fields remain absent. The regression failed before the fix; 563 ExUnit tests and Credo pass afterward.
@darwin67

darwin67 commented Oct 11, 2026 •

Copy link
Copy Markdown
Member Author

Oracle review complete: signed off after resolving the missing reference_url in deleted-list metadata. The removed-paste projection now selects the stored reference while leaving content unloaded. Extended the API regression to verify the exact reference and absence of data/data_base64/data_encoding. The regression failed before the source fix (nil instead of the stored URL) and passes afterward. Final validation: mix precommit (563 ExUnit tests plus JS tests) and mix credo passed. The existing lifecycle API routes remain in /api/v1 with [:api, :require_authenticated_api]; no authentication or route changes were needed for this fix. Fix pushed without rewriting published history. Review session: https://ampcode.com/threads/T-01a12594-e2f1-75be-b343-9239564f5c84

Post-push CI is green: memory/local/S3 Elixir tests, Rust tests, formatting/linting, and production-container build passed. Release/publication jobs skipped as expected.

@darwin67
darwin67 merged commit dec2889 into main Oct 11, 2026
17 checks passed
@darwin67
darwin67 deleted the feat/api-paste-lifecycle branch October 11, 2026 00:28
darwin67 added a commit that referenced this pull request Oct 11, 2026
…es (#72)

## Summary

The second of two stacked PRs. Stacked on #71, whose endpoints it uses.
It brings the Rust client and the CLI up to the redesigned paste model.

**Client (`textbin-client`)**
- Create takes a `CreateOptions` struct and sends `title` and
`reference_url` along with syntax, expiry and visibility.
- Pastes carry their id, title, reference link, share URL, raw URL and
timestamps. All of them are optional, so responses from older servers
still decode.
- New calls: `list_removed_pastes`, `restore_paste` and `purge_paste`.
- Pastes serialize the way the server represents them: text in `data`,
and binary content as base64.

**CLI (`textbin`)**
- `create --title … --reference …` (alias `--ref`) prints the server's
share link.
- `--json` on `create`, `show`, `delete`, `deleted` and `restore` prints
one line of JSON for scripts. This covers the paste commands in RFD 3's
"machine-readable `--json`" item. The `auth` commands are not covered
yet.
- `delete` tells you how to restore the paste. `delete --permanent`
deletes it for good, including one that was already deleted.
- `deleted` lists restorable pastes with their reason, purge date and
title.
- `restore <id>` brings a deleted or expired paste back.

## Test plan

- [x] `cargo fmt --check`, `cargo clippy --workspace --all-features --
-D warnings`
- [x] `cargo test --workspace`: 61 tests, 0 failures. New tests cover
create query params, decoding links, JSON serialization of text and
binary pastes, the deleted-collection requests against a stub server,
`deleted` row formatting and argument parsing.
- [x] End to end against a local server: created a paste with a title
and reference, then ran `show --json`, `delete`, `deleted`, `restore`,
`delete --permanent`, and a restore of the purged paste, which returned
404.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_019fnjMN7WpEjQJD2zveakV6

---
_Generated by [Claude
Code](https://claude.ai/code/session_019fnjMN7WpEjQJD2zveakV6)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Darwin D. Wu <darwin67@users.noreply.github.com>
@chaba2-bot chaba2-bot Bot mentioned this pull request Oct 11, 2026
darwin67 added a commit that referenced this pull request Oct 11, 2026
## Summary

RFD 3 asks for an OpenAPI document that is versioned with the server and
checked for drift in CI. The repo didn't have one. This PR adds the
document and the checks that keep it accurate.

**New dependency:** `open_api_spex` 3.22 (MPL-2.0), the standard Phoenix
library for OpenAPI. The spec is written in Elixir, and the library also
provides the JSON export and the schema casting the tests use. It has no
open security advisories.

**Commits**
1. **`feat`: the document.**
   - `TextbinWeb.ApiSpec` describes all 21 operations:
     - Token creation and identity.
     - Organizations, workspaces, the audit log and workspace recovery.
     - Paste create, list, show and delete.
- The removed-paste lifecycle, under both the personal and the
workspace-scoped routes.
     - Raw paste content.
- `TextbinWeb.ApiSpec.Schemas` holds the request and response schemas.
- `mix openapi.export` writes `priv/openapi.json`, the copy for client
generation, such as the TypeScript SDK the RFD mentions. It also puts
contract changes in front of reviewers.
   - Tests fail if:
- an `/api/v1` route has no operation in the document, or the document
has an operation with no route;
     - a schema reference doesn't resolve;
     - `priv/openapi.json` is out of date.
2. **`test`: contract tests.**
- `assert_api_response(conn, operation_id)` checks a real response
against the operation it implements. The status must be one the document
lists, and a JSON body must match its schema.
- Response schemas are closed (`additionalProperties: false`). An
undocumented field therefore fails a test, just like a missing or
mistyped one.
- Every operation is exercised for its success response and its main
errors.
   - The two RFD 3 items this PR and #71 complete are ticked.

**How drift gets caught.** These are ordinary `mix test` tests, so they
already run in every CI storage matrix job. Changing a JSON view, a
route, or the document without updating the others fails CI. To check
the tests catch something, I added an undocumented `surprise` field to
`PasteJSON` and changed `title` to a number. Each change failed the
tests with a readable message naming the operation and the field.

**When you change the API:** update `TextbinWeb.ApiSpec`, run `mix
openapi.export`, and commit `priv/openapi.json`.

## Test plan

- [x] `mix test`: 580 tests, 0 failures, 12 of them new contract tests.
- [x] `mix credo` reports no issues. `mix compile --warnings-as-errors`
is clean.
- [x] The tests fail on an undocumented response field and on a field
with the wrong type.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_019fnjMN7WpEjQJD2zveakV6

---
_Generated by [Claude
Code](https://claude.ai/code/session_019fnjMN7WpEjQJD2zveakV6)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Darwin D. Wu <darwin67@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants