feat: PubkyAppCollectionPointer (unified own/follow primitive) - #119
Draft
SHAcollision wants to merge 5 commits into
Draft
feat: PubkyAppCollectionPointer (unified own/follow primitive)#119SHAcollision wants to merge 5 commits into
SHAcollision wants to merge 5 commits into
Conversation
SHAcollision
force-pushed
the
feat/spec-v0.5.0-collection-pointer
branch
from
May 14, 2026 13:54
bdf2edf to
142c2f8
Compare
….5.0) (#116) * Add `Collection` post kind + `PubkyAppCollectionContent` envelope (v0.5.0) A collection post curates an ordered list of other posts (URIs in `attachments`) under a `name` and optional `description`. On the wire it is a `PubkyAppPost` with `kind = "collection"` and a typed JSON envelope in `content`; no new homeserver path, no new top-level type. Spec changes: - New `PubkyAppPostKind::Collection` variant (placed before `Unknown` so the `#[serde(other)]` catch-all still works). - `FromStr`, `Display`, and both WASM kind getters handle the variant. - New `PubkyAppCollectionContent { name, description }` struct, parsed but never re-serialized as a top-level homeserver object. Forward-compat by design: NO `deny_unknown_fields`, so future additive fields (e.g. `cover_image`) won't break older parsers. - Three new limits constants: - `collection_content_max_length = 2_000` - `collection_items_max_count = 100` - `collection_item_uri_max_length = 300` - Kind-gated `validate` early-branch for `Collection`: forbids parent and embed, parses the envelope, validates name (1..=100), description (..=500), attachment count (..=100), per-URI length (..=300), and protocol allowlist. Existing kind-switch tuple stays untouched for the six legacy kinds. - Re-export `PubkyAppCollectionContent` from `lib.rs`. - VERSION bumped to 0.5.0. Tests: 17 native + 1 wasm-gated for the `Collection` variant (round-trip, malformed envelope, name/description/items/URI boundaries on both sides, parent/embed rejection, zero-item drafts allowed, forward-compat tolerance of extra envelope fields, protocol allowlist, existing-kinds regression). Phase-1 FromStr-rejects test updated to drop `"collection"` from the unknown-strings list (it's now a known kind). cargo nextest run: 187 / 187 pass. * Apply rustfmt to test module additions * Promote Collection name/description bounds to `VALIDATION_LIMITS` Code-review pass flagged that the `1..=100` (name) and `..=500` (description) magic numbers in the Collection validation branch were hardcoded inline rather than living in `VALIDATION_LIMITS` like the other Collection limits (`collection_content_max_length`, `collection_items_max_count`, `collection_item_uri_max_length`). Drift risk: frontend / WASM consumers serialize `VALIDATION_LIMITS` for client-side hints; with the bounds hardcoded here they can't see the authoritative values. Promoting them keeps `VALIDATION_LIMITS` as the single source of truth. - Add three new fields: - `collection_name_min_length = 1` - `collection_name_max_length = 100` - `collection_description_max_length = 500` - Replace inline literals in `validate` with field reads. Error messages now interpolate the values rather than baking them in (so a future bump only needs to touch `limits.rs`). - Update doc-comments on `PubkyAppCollectionContent::{name,description}` to reference the constants instead of the literals. `cargo nextest run`: 187/187 pass. `cargo fmt --check`: clean. * Add PubkyAppPostKind::is_known() helper Addresses PR #116 review comment. Provides a small inherent helper so consumers — indexers, stream filters, search ranking — can write `if kind.is_known() { ... }` rather than `if !matches!(kind, PubkyAppPostKind::Unknown) { ... }`. No WASM binding: JS consumers already receive `kind` as a string via the existing WASM getter and can check `kind !== "unknown"` directly. * Drop commented-out kind() getter scaffolds Two identical dead-code blocks under PubkyAppPostEmbed::kind() and PubkyAppPost::kind() that previewed a `kind() -> PubkyAppPostKind` alternative incompatible with WASM. No callers, no explanation. * Cover Collection envelope edge cases and dangerous protocols Five tests: - accepts_empty_description: description = "" is valid (0..=500) - accepts_max_description: description = "a".repeat(500) is valid - rejects_missing_name: envelope JSON with no `name` key is rejected - rejects_javascript_protocol: attachment "javascript:..." rejected - rejects_data_uri_protocol: attachment "data:..." rejected The two protocol tests are XSS-vector defenses — `javascript:` and `data:` parse as URLs but must not pass the attachment-protocol allowlist alongside `pubky`/`http`/`https`. * Clarify PubkyAppCollectionContent intent, mark pubky-sdk URI gap, align protocol error Three polish changes for PR #116: - Add a Construction paragraph to PubkyAppCollectionContent's doc-comment explaining that the struct is a deserialization target (parsed from a Post's content envelope) and is not intended to be constructed by callers directly. The pub re-export from lib.rs exists so SDK consumers can inspect the envelope shape (OpenAPI schema, type definitions). - Mark a TODO at the attachment-URL parse site noting that the shortened pubky-sdk absolute URI form `pubky<pubkey>/pub/...` (no `://`) is a valid pubky resource per pubky-core / pubky-sdk but is currently rejected by `Url::parse`. This rejection is systemic across the crate (tag.rs, bookmark.rs, user.rs, file.rs, common.rs, uri_parser.rs); a follow-up issue should add a normalization shim once Collections lands. - Align the Collection attachment-protocol rejection error with the legacy attachment-protocol error at :445-460: enumerate the allowed protocols in the message, include the offending attachment's index, and use the same `must use one of the allowed protocols` phrasing. This makes the two error sites consistent for end users. * Apply rustfmt to Collection envelope/protocol tests * Add WASM ergonomic helper: PubkySpecsBuilder.createCollectionPost() JS callers shouldn't have to JSON.stringify the Collection envelope themselves before passing it as `content` to createPost. This helper takes the structured envelope fields directly: createCollectionPost(name, description?, attachments?) → PostResult and builds the {name, description} envelope internally before delegating to PubkyAppPost::new(..., Collection, ...). parent and embed are omitted from the signature since the validator rejects them for Collection posts. Adds a wasm_bindgen_test exercising the full path (builder construction → envelope serialization → typed PostResult → envelope round-trip via serde_json) so the TS contract has a runnable test. * Address PR #116 review feedback - Apply ok300's docblock fix for PubkyAppCollectionContent (ordered list of URIs, not posts). - Use the new is_known() helper at the two `validate()` sites that reject Unknown kinds (self.kind and embed.kind), per aintnostressin. - Drop the .trim() from Collection-name length validation per ok300: leading/trailing whitespace now counts toward the total. Add an explicit whitespace-only-name guard so " " is still rejected, and update limits.rs docstrings to match. Two new tests cover both the whitespace rejection and the load-bearing assertion that padded names now count their full length toward max. - Replace the verbose pubky-sdk-URI TODO with a brief note; tracking the systemic shim as a separate issue. * Move Collection items into envelope; reserve post.attachments Per aintnostressin's review: Collection items and post attachments are distinct concepts. Conflating them in `post.attachments` left no room for future Collection-level attachments (cover image, metadata). Change: `PubkyAppCollectionContent` gains `items: Vec<String>` (with #[serde(default)] for forward-compat). The Collection validator now validates `envelope.items` and rejects any non-empty `post.attachments` on a Collection — anti-misuse guard that lifts when Collections gain real attachments later. WASM `createCollectionPost` builder routes its third argument into the envelope; same signature shape, renamed `attachments → items` for clarity. Raises `collection_content_max_length: 2_000 → 40_000` (scalar budget; the envelope now holds the items list). Updates tests accordingly, adds 3 regression tests covering the anti-misuse guard, missing-items forward-compat, and the load-bearing max-size envelope. * Revise rustdoc for WASM's createCollectionPost Update documentation for createCollectionPost function to reflect changes in parameters and functionality. --------- Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
Introduces a single spec primitive that serves both Collections
objectives — sovereign homeserver-side listing of your own collections
AND subscription pointers to other users' collections — via one path
shape:
/pub/pubky.app/collections/<owner_id>/<post_id>
with body { created_at: i64 } matching PubkyAppFollow exactly. The
role of a given pointer is inferred at read time by comparing the
path's <owner_id> against the URI host (the homeserver user):
- owner_id == homeserver_user → own-pointer (sovereign index entry).
Indexers do nothing with these; the homeserver state suffices.
- owner_id != homeserver_user → follow-pointer (subscription).
Indexers (Nexus) materialize this as a :FOLLOWS_COLLECTION edge.
The spec encodes no role field, no role subfolder, no role variant.
Wiring:
- new src/models/collection_pointer.rs with HasIdPath/Validatable
impls, WASM bindings (fromJson/toJson/createPath), unit tests.
- one PubkyAppObject::CollectionPointer variant + one from_resource
dispatch arm.
- one Resource::CollectionPointer { owner, post_id } variant +
Display/id()/try_to_uri_str entries + one parser arm placed BEFORE
the generic [res_type, id, ..] (with a guard that falls through
cleanly for non-collections three-segment paths).
- lib.rs re-export + utils.rs URI builder.
Three new parser tests cover the happy path (round-trip via
try_to_uri_str), the trailing-slash → Unknown fallthrough, and the
malformed-owner-pubkey rejection.
The CollectionPointer primitive carries no role field. Whether a given pointer is an own-pointer (sovereign index entry for the user's own collection) or a follow-pointer (subscription to someone else's collection) is determined at read time by comparing the URI host (ParsedUri::user_id) against the path-encoded `owner`. These two PubkyAppObject::from_uri integration tests encode that convention as part of the spec's testable contract: - test_import_collection_pointer_own_role: when the URI host matches the path owner, the parsed Resource carries owner == user_id (consumers treat this as an own-pointer; indexers must skip notification + graph-edge creation). - test_import_collection_pointer_follow_role: when the URI host differs from the path owner, the parsed Resource carries owner != user_id (consumers treat this as a follow-pointer; indexers materialize the :FOLLOWS_COLLECTION edge and fire a notification). Both produce the same PubkyAppObject::CollectionPointer variant; the test is at the Resource layer where the convention is observable.
SHAcollision
force-pushed
the
feat/spec-v0.5.0-collection-pointer
branch
from
May 20, 2026 09:47
57c385e to
634a7ae
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
PubkyAppCollectionPointer— a single spec primitive that serves both Collections objectives:One struct, one path pattern, one
Resourcevariant, onePubkyAppObjectvariant, one parser arm.(body matches
PubkyAppFollowexactly)Role inference
Role is determined at read time by comparing the path's
<owner_id>to the URI host (homeserver user):owner_id == homeserver_user→ own-pointer (sovereign index entry). Indexers do nothing — the homeserver state is enough to list your own collections via a prefix scan on/pub/pubky.app/collections/<self>/.owner_id != homeserver_user→ follow-pointer (subscription). The Nexus watcher (forthcoming PR) materializes this as a(:User)-[:FOLLOWS_COLLECTION]->(:Post {kind:'collection'})edge and emits a follow-notification to the target owner.The spec encodes no role field. Indexers and clients apply the same owner-equality check independently.
Why not reserved tag-label `"follow"`?
The original
collections-plan.mdproposedPubkyAppTag { uri: <collection_uri>, label: \"follow\" }for the follow side. Replaced by this design because:follow,mute,bookmark,friendare all separate primitives — not tag conventions).Sovereign listing
/pub/pubky.app/collections/*/*/pub/pubky.app/collections/<you>/*/pub/pubky.app/collections/*/*and filter on owner != you