-
Notifications
You must be signed in to change notification settings - Fork 7
[HYPER-536] feat(lexicons): add app.certified.graph.entityFollow #235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| --- | ||
| "@hypercerts-org/lexicon": minor | ||
| --- | ||
|
|
||
| Add `app.certified.graph.entityFollow` lexicon — a follow record that targets a **record** rather than an **account**. | ||
|
|
||
| It is structurally identical to `app.certified.graph.follow` (same `key: tid`, same `createdAt`, optional `via` strongRef, and optional `signatures` fields). The only difference is `subject`, which is a `string` with `format: at-uri` referencing the record being followed, instead of a `string` with `format: did` identifying an account. This lets clients express "follow this activity/claim/collection" alongside the existing "follow this account" relationship, without overloading the meaning of `app.certified.graph.follow`. | ||
|
|
||
| Because `subject` is an unconstrained `at-uri`, an entity follow may reference a record in any collection — including one governed by a lexicon outside this repository. Consumers should resolve the referenced record before assuming a particular shape, and treat the collection portion of the URI as untrusted input. | ||
|
|
||
| The new NSID is added to the `app.certified.authWrite` permission set, so apps already requesting `include:app.certified.authWrite` gain create/update/delete on the new collection when the updated set is published. Note that this widens the effective grant of an already-consented set; see `docs/design/permission-sets.md` for the set-growth semantics. | ||
|
|
||
| Exports new `GRAPH_ENTITY_FOLLOW_NSID`, `GRAPH_ENTITY_FOLLOW_LEXICON_JSON`, `GRAPH_ENTITY_FOLLOW_LEXICON_DOC`, and the `AppCertifiedGraphEntityFollow` type namespace. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| { | ||
| "lexicon": 1, | ||
| "id": "app.certified.graph.entityFollow", | ||
| "defs": { | ||
| "main": { | ||
| "type": "record", | ||
| "description": "Record declaring a 'follow' relationship of an entity record rather than an account. Duplicate follows will be ignored by the AppView.", | ||
| "key": "tid", | ||
| "record": { | ||
| "type": "object", | ||
| "required": ["subject", "createdAt"], | ||
| "properties": { | ||
| "subject": { | ||
| "type": "string", | ||
| "format": "at-uri", | ||
| "description": "AT-URI of the entity record being followed." | ||
| }, | ||
|
Comment on lines
+13
to
+17
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The other way of doing this is an open union, as proposed by @Ashex in #236. That would allow follows to be not just weak refs but also strongRefs and anything else people want. That may or may not be practically useful - I'm not sure at this point, but I guess the point is that with an open union we don't have to know or care right now because it's always expandable later. The only minor downside is that it then needs to be wrapped in an object.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| "createdAt": { | ||
| "type": "string", | ||
| "format": "datetime", | ||
| "description": "Client-declared timestamp when this record was originally created." | ||
| }, | ||
| "via": { | ||
| "type": "ref", | ||
| "ref": "com.atproto.repo.strongRef", | ||
| "description": "Optional strong reference to a record that mediated this follow (e.g. a starter pack or other curated list). Mirrors the optional `via` field on app.bsky.graph.follow; the referenced record may conform with any lexicon." | ||
| }, | ||
| "signatures": { | ||
| "type": "ref", | ||
| "ref": "app.certified.signature.defs#list", | ||
| "description": "Optional cryptographic signatures attesting to this record's content." | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { validate, ids } from "../generated/lexicons"; | ||
| import * as EntityFollow from "../generated/types/app/certified/graph/entityFollow"; | ||
|
|
||
| const VALID_AT_URI = | ||
| "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/org.hypercerts.activity/3k2abc"; | ||
| const VALID_DID = "did:plc:ewvi7nxzyoun6zhxrhs64oiz"; | ||
| const VALID_CID = "bafyreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy"; | ||
|
|
||
| describe("app.certified.graph.entityFollow", () => { | ||
| it("should accept a valid entity follow record (subject + createdAt only)", () => { | ||
| const result = EntityFollow.validateMain({ | ||
| $type: ids.AppCertifiedGraphEntityFollow, | ||
| subject: VALID_AT_URI, | ||
| createdAt: "2024-01-01T00:00:00Z", | ||
| }); | ||
| expect(result.success).toBe(true); | ||
| if (result.success) { | ||
| expect(result.value.subject).toBe(VALID_AT_URI); | ||
| } | ||
| }); | ||
|
|
||
| it("should accept an entity follow record with optional via strongRef", () => { | ||
| const result = EntityFollow.validateMain({ | ||
| $type: ids.AppCertifiedGraphEntityFollow, | ||
| subject: VALID_AT_URI, | ||
| createdAt: "2024-01-01T00:00:00Z", | ||
| via: { | ||
| uri: "at://did:plc:alice/app.certified.graph.starterpack/3k2abc", | ||
| cid: VALID_CID, | ||
| }, | ||
| }); | ||
| expect(result.success).toBe(true); | ||
| if (result.success) { | ||
| expect(result.value.via?.uri).toBe( | ||
| "at://did:plc:alice/app.certified.graph.starterpack/3k2abc", | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| // subject is an unconstrained at-uri, so a follow may target a record in | ||
| // any collection -- including lexicons defined outside this repository. | ||
| it("should accept a subject in a collection outside this repository", () => { | ||
| const result = EntityFollow.validateMain({ | ||
| $type: ids.AppCertifiedGraphEntityFollow, | ||
| subject: "at://did:plc:alice/app.bsky.feed.post/3k2abc", | ||
| createdAt: "2024-01-01T00:00:00Z", | ||
| }); | ||
| expect(result.success).toBe(true); | ||
| }); | ||
|
|
||
| it("should reject a record missing required subject", () => { | ||
| const result = validate( | ||
| { createdAt: "2024-01-01T00:00:00Z" }, | ||
| ids.AppCertifiedGraphEntityFollow, | ||
| "main", | ||
| false, | ||
| ); | ||
| expect(result.success).toBe(false); | ||
| }); | ||
|
|
||
| it("should reject a record missing required createdAt", () => { | ||
| const result = validate( | ||
| { subject: VALID_AT_URI }, | ||
| ids.AppCertifiedGraphEntityFollow, | ||
| "main", | ||
| false, | ||
| ); | ||
| expect(result.success).toBe(false); | ||
| }); | ||
|
|
||
| it("should reject a subject that is not a valid AT-URI", () => { | ||
| const result = validate( | ||
| { | ||
| subject: "not-an-at-uri", | ||
| createdAt: "2024-01-01T00:00:00Z", | ||
| }, | ||
| ids.AppCertifiedGraphEntityFollow, | ||
| "main", | ||
| false, | ||
| ); | ||
| expect(result.success).toBe(false); | ||
| }); | ||
|
|
||
| // The distinguishing constraint vs app.certified.graph.follow: a bare DID is | ||
| // a valid subject there, but not here. | ||
| it("should reject a bare DID as subject", () => { | ||
| const result = validate( | ||
| { | ||
| subject: VALID_DID, | ||
| createdAt: "2024-01-01T00:00:00Z", | ||
| }, | ||
| ids.AppCertifiedGraphEntityFollow, | ||
| "main", | ||
| false, | ||
| ); | ||
| expect(result.success).toBe(false); | ||
| }); | ||
|
|
||
| it("should reject an invalid datetime", () => { | ||
| const result = validate( | ||
| { | ||
| subject: VALID_AT_URI, | ||
| createdAt: "not-a-datetime", | ||
| }, | ||
| ids.AppCertifiedGraphEntityFollow, | ||
| "main", | ||
| false, | ||
| ); | ||
| expect(result.success).toBe(false); | ||
| }); | ||
|
|
||
| it("should reject a via that is not a valid strongRef", () => { | ||
| const result = validate( | ||
| { | ||
| subject: VALID_AT_URI, | ||
| createdAt: "2024-01-01T00:00:00Z", | ||
| via: { uri: "at://did:plc:alice/app.certified.graph.starterpack/x" }, // missing cid | ||
| }, | ||
| ids.AppCertifiedGraphEntityFollow, | ||
| "main", | ||
| false, | ||
| ); | ||
| expect(result.success).toBe(false); | ||
| }); | ||
|
|
||
| it("should require $type when requiredType is true", () => { | ||
| const result = validate( | ||
| { | ||
| subject: VALID_AT_URI, | ||
| createdAt: "2024-01-01T00:00:00Z", | ||
| }, | ||
| ids.AppCertifiedGraphEntityFollow, | ||
| "main", | ||
| true, | ||
| ); | ||
| expect(result.success).toBe(false); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.