Skip to content

Commit 464fc34

Browse files
authored
feat(api): add ActivityPub schema parity proof (#335)
* feat(api): add ActivityPub schema parity proof * fix(api): tighten ActivityPub schema proof Guarantee schema parity with the Mastodon actor fixture and remove Schema.Unknown from the ActivityPub API boundary. Proof obligations: required ActivityPub object fields decode iff their literal type/context invariants hold; ordered collection items and ForgeFed raw payloads are JSON values; unsupported followers pages fail with typed 400 errors. * fix(api): enforce ActivityPub actor extension shapes Close the CodeRabbit proof gap by replacing permissive actor extension records with literal discriminated schemas for Image, PropertyValue, Hashtag, and Emoji. The interactionPolicy contract now accepts only Mastodon canFeature/canQuote approval blocks with at least one approval list, and regression tests reject the prior empty/arbitrary payload counterexamples. * fix(api): enforce exact ActivityPub parity schemas
1 parent 9691b4c commit 464fc34

7 files changed

Lines changed: 1102 additions & 80 deletions

File tree

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import * as Schema from "effect/Schema"
2+
import type * as SchemaAST from "effect/SchemaAST"
3+
4+
import {
5+
activityStreamsJsonLdContext,
6+
forgeFedJsonLdContext,
7+
securityJsonLdContext,
8+
socialWebWebfingerJsonLdContext
9+
} from "./contracts.js"
10+
11+
export type JsonPrimitive = boolean | number | string | null
12+
export type JsonValue = JsonPrimitive | JsonObject | ReadonlyArray<JsonValue>
13+
export type JsonObject = Readonly<{ [key: string]: JsonValue }>
14+
15+
export const exactActivityPubParseOptions: SchemaAST.ParseOptions = {
16+
onExcessProperty: "error"
17+
}
18+
19+
export const JsonValueSchema: Schema.Schema<JsonValue> = Schema.suspend(() =>
20+
Schema.Union(
21+
Schema.Null,
22+
Schema.Boolean,
23+
Schema.Number,
24+
Schema.String,
25+
Schema.Array(JsonValueSchema),
26+
Schema.Record({ key: Schema.String, value: JsonValueSchema })
27+
)
28+
)
29+
30+
const OptionalString = Schema.optional(Schema.String)
31+
const JsonObjectSchema = Schema.Record({ key: Schema.String, value: JsonValueSchema })
32+
const JsonLdContextEntrySchema = Schema.Union(Schema.String, JsonObjectSchema)
33+
const JsonLdIdMappingSchema = Schema.Struct({
34+
"@id": Schema.String,
35+
"@type": Schema.Literal("@id")
36+
})
37+
38+
export const ActivityForgeFedJsonLdContextSchema = Schema.Tuple(
39+
Schema.Literal(activityStreamsJsonLdContext),
40+
Schema.Literal(forgeFedJsonLdContext)
41+
)
42+
43+
export const LocalActorJsonLdContextSchema = Schema.Tuple(
44+
Schema.Literal(activityStreamsJsonLdContext),
45+
Schema.Literal(securityJsonLdContext),
46+
Schema.Literal(forgeFedJsonLdContext)
47+
)
48+
49+
export const MastodonActorContextExtensionsSchema = Schema.Struct({
50+
manuallyApprovesFollowers: Schema.String,
51+
toot: Schema.String,
52+
featured: JsonLdIdMappingSchema,
53+
featuredTags: JsonLdIdMappingSchema,
54+
alsoKnownAs: JsonLdIdMappingSchema,
55+
movedTo: JsonLdIdMappingSchema,
56+
schema: Schema.String,
57+
PropertyValue: Schema.String,
58+
value: Schema.String,
59+
discoverable: Schema.String,
60+
suspended: Schema.String,
61+
memorial: Schema.String,
62+
indexable: Schema.String,
63+
attributionDomains: JsonLdIdMappingSchema,
64+
showFeatured: Schema.String,
65+
showMedia: Schema.String,
66+
showRepliesInMedia: Schema.String,
67+
gts: Schema.String,
68+
interactionPolicy: JsonLdIdMappingSchema,
69+
canQuote: JsonLdIdMappingSchema,
70+
automaticApproval: JsonLdIdMappingSchema,
71+
manualApproval: JsonLdIdMappingSchema
72+
})
73+
74+
export const MastodonActorJsonLdContextSchema = Schema.Tuple(
75+
Schema.Literal(activityStreamsJsonLdContext),
76+
Schema.Literal(securityJsonLdContext),
77+
Schema.Literal(socialWebWebfingerJsonLdContext),
78+
MastodonActorContextExtensionsSchema
79+
)
80+
81+
export const ActorJsonLdContextSchema = Schema.Union(
82+
LocalActorJsonLdContextSchema,
83+
MastodonActorJsonLdContextSchema
84+
)
85+
86+
export const JsonLdContextSchema = Schema.Union(
87+
Schema.String,
88+
JsonObjectSchema,
89+
Schema.Array(JsonLdContextEntrySchema)
90+
)
91+
92+
export const ForgeFedTicketSourceSchema = Schema.Struct({
93+
content: OptionalString,
94+
mediaType: OptionalString
95+
})
96+
97+
export const ForgeFedTicketSchema = Schema.Struct({
98+
id: Schema.String,
99+
attributedTo: Schema.String,
100+
summary: Schema.String,
101+
content: Schema.String,
102+
mediaType: OptionalString,
103+
source: Schema.optional(Schema.Union(Schema.String, ForgeFedTicketSourceSchema)),
104+
published: OptionalString,
105+
updated: OptionalString,
106+
url: OptionalString,
107+
context: OptionalString,
108+
workType: OptionalString,
109+
attachment: Schema.optional(Schema.Array(JsonValueSchema)),
110+
raw: Schema.optional(JsonValueSchema)
111+
})
112+
113+
export const ActivityPubPublicKeySchema = Schema.Struct({
114+
id: Schema.String,
115+
owner: Schema.String,
116+
publicKeyPem: Schema.String
117+
})
118+
119+
const ActivityPubEndpointsSchema = Schema.Struct({
120+
sharedInbox: Schema.String
121+
})
122+
123+
const ActivityPubImageSchema = Schema.Struct({
124+
type: Schema.Literal("Image"),
125+
mediaType: OptionalString,
126+
url: Schema.String,
127+
name: OptionalString
128+
})
129+
130+
const ActivityPubActorAttachmentSchema = Schema.Struct({
131+
type: Schema.Literal("PropertyValue"),
132+
name: Schema.String,
133+
value: Schema.String
134+
})
135+
136+
const ActivityPubHashtagTagSchema = Schema.Struct({
137+
type: Schema.Literal("Hashtag"),
138+
name: Schema.String,
139+
href: Schema.String
140+
})
141+
142+
const ActivityPubEmojiTagSchema = Schema.Struct({
143+
type: Schema.Literal("Emoji"),
144+
id: Schema.String,
145+
name: Schema.String,
146+
icon: ActivityPubImageSchema,
147+
updated: OptionalString
148+
})
149+
150+
const ActivityPubActorTagSchema = Schema.Union(
151+
ActivityPubHashtagTagSchema,
152+
ActivityPubEmojiTagSchema
153+
)
154+
155+
const ActivityPubInteractionApprovalSchema = Schema.Struct({
156+
automaticApproval: Schema.optional(Schema.Array(Schema.String)),
157+
manualApproval: Schema.optional(Schema.Array(Schema.String))
158+
}).pipe(
159+
Schema.filter((approval) =>
160+
approval.automaticApproval !== undefined ||
161+
approval.manualApproval !== undefined)
162+
)
163+
164+
const MastodonInteractionPolicySchema = Schema.Struct({
165+
canFeature: Schema.optional(ActivityPubInteractionApprovalSchema),
166+
canQuote: Schema.optional(ActivityPubInteractionApprovalSchema)
167+
}).pipe(
168+
Schema.filter((policy) =>
169+
policy.canFeature !== undefined ||
170+
policy.canQuote !== undefined)
171+
)
172+
173+
export const LocalActivityPubPersonSchema = Schema.Struct({
174+
"@context": LocalActorJsonLdContextSchema,
175+
type: Schema.Literal("Person"),
176+
id: Schema.String,
177+
name: Schema.String,
178+
preferredUsername: Schema.String,
179+
summary: Schema.String,
180+
inbox: Schema.String,
181+
outbox: Schema.String,
182+
followers: Schema.String,
183+
following: Schema.String,
184+
liked: Schema.String,
185+
publicKey: ActivityPubPublicKeySchema,
186+
endpoints: ActivityPubEndpointsSchema
187+
})
188+
189+
export const MastodonIssueActivityPubPersonSchema = Schema.Struct({
190+
"@context": MastodonActorJsonLdContextSchema,
191+
id: Schema.String,
192+
webfinger: Schema.String,
193+
type: Schema.Literal("Person"),
194+
following: Schema.String,
195+
followers: Schema.String,
196+
inbox: Schema.String,
197+
outbox: Schema.String,
198+
featured: Schema.String,
199+
featuredTags: Schema.String,
200+
preferredUsername: Schema.String,
201+
name: Schema.String,
202+
summary: Schema.String,
203+
url: Schema.String,
204+
manuallyApprovesFollowers: Schema.Boolean,
205+
discoverable: Schema.Boolean,
206+
indexable: Schema.Boolean,
207+
published: Schema.String,
208+
memorial: Schema.Boolean,
209+
showFeatured: Schema.Boolean,
210+
showMedia: Schema.Boolean,
211+
showRepliesInMedia: Schema.Boolean,
212+
interactionPolicy: MastodonInteractionPolicySchema,
213+
featuredCollections: Schema.String,
214+
publicKey: ActivityPubPublicKeySchema,
215+
tag: Schema.Array(ActivityPubActorTagSchema),
216+
attachment: Schema.Array(ActivityPubActorAttachmentSchema),
217+
endpoints: ActivityPubEndpointsSchema
218+
})
219+
220+
export const ActivityPubPersonSchema = Schema.Union(
221+
LocalActivityPubPersonSchema,
222+
MastodonIssueActivityPubPersonSchema
223+
)
224+
225+
export const ActivityPubFollowActivitySchema = Schema.Struct({
226+
"@context": ActivityForgeFedJsonLdContextSchema,
227+
id: Schema.String,
228+
type: Schema.Literal("Follow"),
229+
actor: Schema.String,
230+
object: Schema.String,
231+
to: Schema.optional(Schema.Array(Schema.String)),
232+
capability: OptionalString
233+
})
234+
235+
export const LocalActivityPubOrderedCollectionSchema = Schema.Struct({
236+
"@context": ActivityForgeFedJsonLdContextSchema,
237+
type: Schema.Literal("OrderedCollection"),
238+
id: Schema.String,
239+
totalItems: Schema.Number,
240+
orderedItems: Schema.Array(JsonValueSchema)
241+
})
242+
243+
export const LocalActivityPubFollowersCollectionSchema = Schema.Struct({
244+
"@context": ActivityForgeFedJsonLdContextSchema,
245+
type: Schema.Literal("OrderedCollection"),
246+
id: Schema.String,
247+
totalItems: Schema.Number,
248+
first: Schema.String,
249+
orderedItems: Schema.Array(JsonValueSchema)
250+
})
251+
252+
export const MastodonFollowersOrderedCollectionSchema = Schema.Struct({
253+
"@context": Schema.Literal(activityStreamsJsonLdContext),
254+
id: Schema.String,
255+
type: Schema.Literal("OrderedCollection"),
256+
totalItems: Schema.Number,
257+
first: Schema.String
258+
})
259+
260+
export const ActivityPubOrderedCollectionSchema = Schema.Union(
261+
LocalActivityPubOrderedCollectionSchema,
262+
LocalActivityPubFollowersCollectionSchema,
263+
MastodonFollowersOrderedCollectionSchema
264+
)
265+
266+
export const LocalActivityPubOrderedCollectionPageSchema = Schema.Struct({
267+
"@context": ActivityForgeFedJsonLdContextSchema,
268+
type: Schema.Literal("OrderedCollectionPage"),
269+
id: Schema.String,
270+
totalItems: Schema.Number,
271+
partOf: Schema.String,
272+
orderedItems: Schema.Array(JsonValueSchema)
273+
})
274+
275+
export const MastodonFollowersOrderedCollectionPageSchema = Schema.Struct({
276+
"@context": Schema.Literal(activityStreamsJsonLdContext),
277+
id: Schema.String,
278+
type: Schema.Literal("OrderedCollectionPage"),
279+
totalItems: Schema.Number,
280+
partOf: Schema.String,
281+
next: Schema.String,
282+
orderedItems: Schema.Array(Schema.String)
283+
})
284+
285+
export const ActivityPubOrderedCollectionPageSchema = Schema.Union(
286+
LocalActivityPubOrderedCollectionPageSchema,
287+
MastodonFollowersOrderedCollectionPageSchema
288+
)

0 commit comments

Comments
 (0)