stash
This commit is contained in:
parent
2bb6624d50
commit
7276750c66
15 changed files with 552 additions and 169 deletions
77
docs/design-in-progress/api-classification.md
Normal file
77
docs/design-in-progress/api-classification.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# API Endpoint Versioning Scope (Design in Progress)
|
||||
|
||||
Status: decision deferred. The `/api/` prefix has already been changed to `/api/v1/` for every
|
||||
route under that prefix (inventory items, storage locations, search, friends, friend requests,
|
||||
groups, group invites, id mapping, tags/properties/categories/availability policies/info,
|
||||
files/item files/staged files, and account export/import/delete). Whether that blanket approach is
|
||||
the right scope, or whether versioning should be narrower, is the open question below.
|
||||
|
||||
## Problem
|
||||
|
||||
A versioned URL prefix matters most where two independently-deployed servers need to agree on a
|
||||
wire contract: federation. An endpoint that's only ever called by a server's own frontend against
|
||||
its own backend is deployed in lockstep with that backend, so it has no independent version to
|
||||
track. Applying a uniform prefix everywhere is simpler, but it also prefixes a large number of
|
||||
routes that never cross a domain boundary, and skips one non-`/api/` route
|
||||
(`/auth/user/<handle>/`, used to fetch a friend's profile/avatar) and the file/thumbnail serving
|
||||
routes (`/media/...`), both of which do cross a domain boundary and arguably belong in the
|
||||
versioned surface more than some of the routes currently in it.
|
||||
|
||||
One exception either way: the version/capability discovery route (`GET /api/version/`) has to stay
|
||||
unprefixed no matter which option is chosen — it's how a caller finds out which versions a server
|
||||
speaks in the first place, so it can't itself live behind a version segment.
|
||||
|
||||
## Options
|
||||
|
||||
**Option A: version every real endpoint uniformly.**
|
||||
Every route under `/api/`, `/auth/`, `/admin/`, and `/media/` gets the `/api/v1/` prefix (except
|
||||
`/api/version/`), regardless of whether it's ever called across a domain boundary. Simple to state
|
||||
and apply; doesn't require maintaining a classification of which routes are federation-facing.
|
||||
|
||||
**Option B: version only routes that actually cross a domain boundary.**
|
||||
Only routes a server's own frontend calls against a *different* server (directly, or that another
|
||||
server calls against this one) get the `/api/v1/` prefix. Everything else keeps its existing,
|
||||
unprefixed path. Smaller versioned surface, but requires keeping the classification below current
|
||||
as routes change, and means a single URL path can't ever serve both a same-domain and
|
||||
cross-domain purpose without the whole path being versioned.
|
||||
|
||||
## Current classification
|
||||
|
||||
Verified against actual call sites (which routes are dispatched against a friend's/foreign
|
||||
server rather than the caller's own home server), not just against which routes are technically
|
||||
reachable by a non-local caller — several routes accept a broader signature-based authentication
|
||||
than they actually need, and one route family enforces "local caller only" itself even though its
|
||||
authentication layer would allow a remote identity through.
|
||||
|
||||
**Crosses a domain boundary today:**
|
||||
- `POST /friendrequests/` (also called against the recipient's own server, not just the sender's)
|
||||
- `GET/POST /groupinvites/` (invite delivery is posted to the invitee's own server)
|
||||
- `POST /group_invites/accept/` (posted to the group's own home server)
|
||||
- `GET /groups/<handle>/`, `DELETE /groups/<handle>/members/<id>/`, `POST /groups/<handle>/invites/`
|
||||
- `* /inventory_items/<handle>/...`, `* /storage_locations/<handle>/...` (group-owned or
|
||||
friend-owned items/locations)
|
||||
- `GET /search/` (fans out to every known server)
|
||||
- `GET /resolve_short_id/<kind>/<owner_id>/<local_id>/`
|
||||
- `GET /auth/user/<handle>/` (friend profile/avatar lookup — not currently under `/api/`)
|
||||
- `GET /media/...`, `GET /thumbnail/<size>/...` (image bytes fetched from the owning server —
|
||||
not currently under `/api/`)
|
||||
|
||||
**Never observed leaving the caller's own home server:**
|
||||
- `GET/POST /friends/`, `DELETE /friends/<id>/`, `DELETE /friendrequests/<id>/`
|
||||
- `GET /groups/` (the collection route, as opposed to `/groups/<handle>/`)
|
||||
- `DELETE /groupinvites/<id>/`, `POST /groupinvites/<id>/accept/`, `GET /groupmemberships/`
|
||||
- `GET /idmap/` (distinct from `/resolve_short_id/...`, which does cross domains)
|
||||
- `* /workflows/...`
|
||||
- `GET /tags/`, `GET /properties/`, `GET /categories/`, `GET /availability_policies/`, `GET /info/`,
|
||||
`GET /domains/` (also not currently called from the frontend at all)
|
||||
- `POST /import/`, `GET /export/`, `DELETE /account_data/`, `DELETE /account/` — these accept the
|
||||
same broad authentication as the federation-facing routes, but reject any non-local caller
|
||||
themselves
|
||||
- `GET /files/`, `* /item_files/...`, `* /staged_files/...`
|
||||
- `GET/PATCH /auth/user/`, `POST /auth/register/`, `POST /auth/token/`, `GET /auth/preferences/`,
|
||||
`GET/PUT /auth/self/preferences/`, `DELETE /auth/self/preferences/<key>/`, `* /auth/users/...`
|
||||
(admin management)
|
||||
- everything under `/admin/` (domain/category/property/tag administration)
|
||||
|
||||
**Exception regardless of which option is chosen:**
|
||||
- `GET /api/version/`
|
||||
|
|
@ -50,16 +50,20 @@ separate "discover groups you're not in" browsing for MVP — you land in a grou
|
|||
it, the same way you become friends with someone by request/accept, not by browsing a directory of
|
||||
all users.
|
||||
|
||||
Known limitation: this list only ever queries the member's own home backend, so it only shows
|
||||
groups actually hosted there (groups you created, or joined on your own domain). Membership itself
|
||||
works regardless of which backend hosts the group — a remote member can still be invited, accept,
|
||||
and fully edit/delete the group's items (see "Owning items as a group" below) — but a group hosted
|
||||
on someone else's backend won't show up in your own "My Groups" list, because unlike friendship
|
||||
(which both sides record), group membership is only ever recorded on the group's own home backend,
|
||||
and there's no index anywhere of "which other backends has this identity been added to." Making a
|
||||
remote membership discoverable would need a small personal pointer index (written by the client at
|
||||
join time) plus a handle-based group lookup on the group's own backend; deferred as a fast-follow
|
||||
alongside group-friending.
|
||||
Listing groups (the table itself) only ever queries the member's own home backend — it's this
|
||||
backend's own view of "groups I host you in" — but every row links to the same group detail page
|
||||
regardless of where the group is actually hosted. Mirroring friendship (which both sides record),
|
||||
the member's own home backend also remembers the bare fact of a remote membership, as a pointer
|
||||
alongside the group's own home backend's real membership record, and merges the two into one table
|
||||
keyed by handle (a group present in both keeps its home-hosted row, which already carries a member
|
||||
count).
|
||||
|
||||
A group's full detail resolves from its handle the same way a friend's foreign items already do:
|
||||
the client resolves the target group's own domain from its handle and talks to that backend
|
||||
directly, rather than assuming home. `GroupDetail.vue` needs no "is this group local or foreign"
|
||||
branch anywhere, including for creating a new item/storage location — the owner selector binds
|
||||
directly to the group's handle, and a group reference is addressed by handle everywhere it appears,
|
||||
never by a bare internal id.
|
||||
|
||||
### Group detail page
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ long-lived, network-interposing piece of code — exactly the kind of thing a su
|
|||
or an XSS-planted `registration.update()` would target. If the page's message handler blindly signs
|
||||
whatever URL the request names, a compromised SW stops being "something that can read images this
|
||||
identity can already see" and becomes "something that can get a validly-signed request for *any*
|
||||
endpoint" — e.g. `POST /api/inventory/items/5/delete` or `POST /api/friends/accept` — and then just
|
||||
endpoint" — e.g. `POST /api/v1/inventory/items/5/delete` or `POST /api/v1/friends/accept` — and then just
|
||||
replay it directly against the real backend. That's a full account-takeover primitive smuggled in
|
||||
through what was supposed to be an image-caching optimization, and it's strictly worse than not
|
||||
having the bridge at all.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# Item Handles & Physical Labels (Design in Progress)
|
||||
|
||||
Status: not implemented. This document collects the problem, goals, and open design questions for
|
||||
giving inventory items stable identifiers and physical (scannable) labels. Nothing here is
|
||||
settled.
|
||||
Status: partially implemented. This document collects the problem, goals, and open design
|
||||
questions for giving inventory items stable identifiers and physical (scannable) labels. The core
|
||||
handle/URL shape question below is settled, generalized to cover any owned kind, not just items —
|
||||
see [User-Qualified ID](../handles-and-shortids.md#user-qualified-id) and
|
||||
[Item URL](../glossary.md#item-url). The remaining open questions (unguessability, label retirement
|
||||
on deletion, route reconciliation, lending/borrowing) are not.
|
||||
|
||||
## Problem
|
||||
|
||||
|
|
@ -59,11 +62,16 @@ unguessability question below).
|
|||
Two distinct formats are needed, because "an item handle" is used in two different situations:
|
||||
|
||||
- *A compact handle, for use where context already makes clear it's a Toolshed item.* Inside the
|
||||
app, in exports, in logs, anywhere the reader already knows they're looking at Toolshed data,
|
||||
the handle doesn't need to spell that out or be openable on its own. This can be as short as
|
||||
`user@domain.tld:id`, the item's owner handle with `:id` appended, mirroring how a tag/category
|
||||
handle already appends `:name` after its origin (see federation.md's Unique Handles section).
|
||||
No new delimiter concept, just the same pattern applied to items.
|
||||
app, in exports, in logs, anywhere the reader already knows they're looking at Toolshed data, the
|
||||
handle doesn't need to spell that out or be openable on its own. An item's compact handle is the
|
||||
item-kind instance of the general [User-Qualified
|
||||
ID](../handles-and-shortids.md#user-qualified-id) scheme: the item's owner handle, the `i` kind
|
||||
letter, and the local id, e.g. `alice@example.com:i42`. The same scheme covers any owned kind (a
|
||||
storage location, for instance); item handles are just the motivating case. An alternative in the
|
||||
same compact-handle family pairs the owner's domain with a [short
|
||||
id](../handles-and-shortids.md#short-ids) instead, e.g. `domain.tld:~DyU` (see
|
||||
[Domain-Qualified Short ID](../handles-and-shortids.md#domain-qualified-short-id)) — shorter, at
|
||||
the cost of needing the app's short id decoder to make sense of it at all.
|
||||
|
||||
- *A self-contained URL, for use with no context at all.* A physical label, a link shared outside
|
||||
the app, has to work without the reader already knowing what it is or which server it belongs
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue