Skip to content

Commit a5bcc9b

Browse files
committed
fix(api): enforce exact ActivityPub parity schemas
1 parent 5d88686 commit a5bcc9b

7 files changed

Lines changed: 345 additions & 203 deletions

File tree

packages/api/src/api/activitypub-schema.ts

Lines changed: 116 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as Schema from "effect/Schema"
2+
import type * as SchemaAST from "effect/SchemaAST"
23

34
import {
45
activityStreamsJsonLdContext,
@@ -11,6 +12,10 @@ export type JsonPrimitive = boolean | number | string | null
1112
export type JsonValue = JsonPrimitive | JsonObject | ReadonlyArray<JsonValue>
1213
export type JsonObject = Readonly<{ [key: string]: JsonValue }>
1314

15+
export const exactActivityPubParseOptions: SchemaAST.ParseOptions = {
16+
onExcessProperty: "error"
17+
}
18+
1419
export const JsonValueSchema: Schema.Schema<JsonValue> = Schema.suspend(() =>
1520
Schema.Union(
1621
Schema.Null,
@@ -23,13 +28,12 @@ export const JsonValueSchema: Schema.Schema<JsonValue> = Schema.suspend(() =>
2328
)
2429

2530
const OptionalString = Schema.optional(Schema.String)
26-
const OptionalBoolean = Schema.optional(Schema.Boolean)
2731
const JsonObjectSchema = Schema.Record({ key: Schema.String, value: JsonValueSchema })
2832
const JsonLdContextEntrySchema = Schema.Union(Schema.String, JsonObjectSchema)
2933
const JsonLdIdMappingSchema = Schema.Struct({
3034
"@id": Schema.String,
31-
"@type": OptionalString
32-
}, Schema.Record({ key: Schema.String, value: JsonValueSchema }))
35+
"@type": Schema.Literal("@id")
36+
})
3337

3438
export const ActivityForgeFedJsonLdContextSchema = Schema.Tuple(
3539
Schema.Literal(activityStreamsJsonLdContext),
@@ -43,29 +47,29 @@ export const LocalActorJsonLdContextSchema = Schema.Tuple(
4347
)
4448

4549
export const MastodonActorContextExtensionsSchema = Schema.Struct({
46-
manuallyApprovesFollowers: OptionalString,
47-
toot: OptionalString,
48-
featured: Schema.optional(JsonLdIdMappingSchema),
49-
featuredTags: Schema.optional(JsonLdIdMappingSchema),
50-
alsoKnownAs: Schema.optional(JsonLdIdMappingSchema),
51-
movedTo: Schema.optional(JsonLdIdMappingSchema),
52-
schema: OptionalString,
53-
PropertyValue: OptionalString,
54-
value: OptionalString,
55-
discoverable: OptionalString,
56-
suspended: OptionalString,
57-
memorial: OptionalString,
58-
indexable: OptionalString,
59-
attributionDomains: Schema.optional(JsonLdIdMappingSchema),
60-
showFeatured: OptionalString,
61-
showMedia: OptionalString,
62-
showRepliesInMedia: OptionalString,
63-
gts: OptionalString,
64-
interactionPolicy: Schema.optional(JsonLdIdMappingSchema),
65-
canQuote: Schema.optional(JsonLdIdMappingSchema),
66-
automaticApproval: Schema.optional(JsonLdIdMappingSchema),
67-
manualApproval: Schema.optional(JsonLdIdMappingSchema)
68-
}, Schema.Record({ key: Schema.String, value: JsonValueSchema }))
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+
})
6973

7074
export const MastodonActorJsonLdContextSchema = Schema.Tuple(
7175
Schema.Literal(activityStreamsJsonLdContext),
@@ -113,7 +117,7 @@ export const ActivityPubPublicKeySchema = Schema.Struct({
113117
})
114118

115119
const ActivityPubEndpointsSchema = Schema.Struct({
116-
sharedInbox: OptionalString
120+
sharedInbox: Schema.String
117121
})
118122

119123
const ActivityPubImageSchema = Schema.Struct({
@@ -166,45 +170,58 @@ const MastodonInteractionPolicySchema = Schema.Struct({
166170
policy.canQuote !== undefined)
167171
)
168172

169-
export const ActivityPubPersonSchema = Schema.Struct({
170-
"@context": ActorJsonLdContextSchema,
173+
export const LocalActivityPubPersonSchema = Schema.Struct({
174+
"@context": LocalActorJsonLdContextSchema,
171175
type: Schema.Literal("Person"),
172176
id: Schema.String,
173-
name: OptionalString,
177+
name: Schema.String,
174178
preferredUsername: Schema.String,
175179
summary: Schema.String,
176180
inbox: Schema.String,
177181
outbox: Schema.String,
178182
followers: Schema.String,
179183
following: Schema.String,
180-
liked: OptionalString,
181-
publicKey: Schema.optional(ActivityPubPublicKeySchema),
182-
endpoints: Schema.optional(ActivityPubEndpointsSchema),
183-
webfinger: OptionalString,
184-
featured: OptionalString,
185-
featuredTags: OptionalString,
186-
url: OptionalString,
187-
manuallyApprovesFollowers: OptionalBoolean,
188-
discoverable: OptionalBoolean,
189-
indexable: OptionalBoolean,
190-
published: OptionalString,
191-
memorial: OptionalBoolean,
192-
alsoKnownAs: Schema.optional(Schema.Array(Schema.String)),
193-
movedTo: OptionalString,
194-
suspended: OptionalBoolean,
195-
attributionDomains: Schema.optional(Schema.Array(Schema.String)),
196-
icon: Schema.optional(ActivityPubImageSchema),
197-
image: Schema.optional(ActivityPubImageSchema),
198-
devices: OptionalString,
199-
showFeatured: OptionalBoolean,
200-
showMedia: OptionalBoolean,
201-
showRepliesInMedia: OptionalBoolean,
202-
interactionPolicy: Schema.optional(MastodonInteractionPolicySchema),
203-
featuredCollections: OptionalString,
204-
tag: Schema.optional(Schema.Array(ActivityPubActorTagSchema)),
205-
attachment: Schema.optional(Schema.Array(ActivityPubActorAttachmentSchema))
184+
liked: Schema.String,
185+
publicKey: ActivityPubPublicKeySchema,
186+
endpoints: ActivityPubEndpointsSchema
206187
})
207188

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+
208225
export const ActivityPubFollowActivitySchema = Schema.Struct({
209226
"@context": ActivityForgeFedJsonLdContextSchema,
210227
id: Schema.String,
@@ -215,24 +232,57 @@ export const ActivityPubFollowActivitySchema = Schema.Struct({
215232
capability: OptionalString
216233
})
217234

218-
export const ActivityPubOrderedCollectionSchema = Schema.Struct({
219-
"@context": JsonLdContextSchema,
235+
export const LocalActivityPubOrderedCollectionSchema = Schema.Struct({
236+
"@context": ActivityForgeFedJsonLdContextSchema,
220237
type: Schema.Literal("OrderedCollection"),
221238
id: Schema.String,
222239
totalItems: Schema.Number,
223-
first: OptionalString,
224-
last: OptionalString,
225-
current: OptionalString,
226-
orderedItems: Schema.optionalWith(Schema.Array(JsonValueSchema), { default: () => [] })
240+
orderedItems: Schema.Array(JsonValueSchema)
227241
})
228242

229-
export const ActivityPubOrderedCollectionPageSchema = Schema.Struct({
230-
"@context": JsonLdContextSchema,
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,
231268
type: Schema.Literal("OrderedCollectionPage"),
232269
id: Schema.String,
233270
totalItems: Schema.Number,
234271
partOf: Schema.String,
235-
next: OptionalString,
236-
prev: OptionalString,
237272
orderedItems: Schema.Array(JsonValueSchema)
238273
})
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+
)

packages/api/src/api/contracts.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import type {
99
ActivityPubPublicKeySchema,
1010
ActorJsonLdContextSchema,
1111
ForgeFedTicketSchema,
12-
ForgeFedTicketSourceSchema
12+
ForgeFedTicketSourceSchema,
13+
LocalActivityPubFollowersCollectionSchema,
14+
LocalActivityPubOrderedCollectionPageSchema,
15+
LocalActivityPubOrderedCollectionSchema,
16+
LocalActivityPubPersonSchema
1317
} from "./activitypub-schema.js"
1418

1519
export type ProjectStatus = "running" | "stopped" | "unknown"
@@ -622,10 +626,19 @@ export type ActivityPubPublicKey = Schema.Schema.Type<typeof ActivityPubPublicKe
622626

623627
export type ActivityPubPerson = Schema.Schema.Type<typeof ActivityPubPersonSchema>
624628

629+
export type LocalActivityPubPerson = Schema.Schema.Type<typeof LocalActivityPubPersonSchema>
630+
625631
export type ActivityPubOrderedCollection = Schema.Schema.Type<typeof ActivityPubOrderedCollectionSchema>
626632

633+
export type LocalActivityPubOrderedCollection =
634+
| Schema.Schema.Type<typeof LocalActivityPubOrderedCollectionSchema>
635+
| Schema.Schema.Type<typeof LocalActivityPubFollowersCollectionSchema>
636+
627637
export type ActivityPubOrderedCollectionPage = Schema.Schema.Type<typeof ActivityPubOrderedCollectionPageSchema>
628638

639+
export type LocalActivityPubOrderedCollectionPage =
640+
Schema.Schema.Type<typeof LocalActivityPubOrderedCollectionPageSchema>
641+
629642
export type FollowSubscription = {
630643
readonly id: string
631644
readonly activityId: string

packages/api/src/api/schema.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ export {
1111
ForgeFedTicketSchema,
1212
ForgeFedTicketSourceSchema,
1313
JsonLdContextSchema,
14-
JsonValueSchema
14+
JsonValueSchema,
15+
LocalActivityPubFollowersCollectionSchema,
16+
LocalActivityPubOrderedCollectionPageSchema,
17+
LocalActivityPubOrderedCollectionSchema,
18+
LocalActivityPubPersonSchema,
19+
MastodonFollowersOrderedCollectionPageSchema,
20+
MastodonFollowersOrderedCollectionSchema,
21+
MastodonIssueActivityPubPersonSchema,
22+
exactActivityPubParseOptions
1523
} from "./activitypub-schema.js"
1624

1725
const OptionalString = Schema.optional(Schema.String)

packages/api/src/http.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ import {
4444
StateCommitRequestSchema,
4545
StateInitRequestSchema,
4646
StateSyncRequestSchema,
47-
UpProjectRequestSchema
47+
UpProjectRequestSchema,
48+
exactActivityPubParseOptions
4849
} from "./api/schema.js"
4950
import type { UpProjectRequestInput } from "./api/schema.js"
5051
import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers"
@@ -316,7 +317,7 @@ const validatedJsonLdResponse = <A, I>(
316317
label: string,
317318
status: number
318319
) =>
319-
Schema.decodeUnknown(schema)(data).pipe(
320+
Schema.decodeUnknown(schema, exactActivityPubParseOptions)(data).pipe(
320321
Effect.mapError((error) =>
321322
new ApiInternalError({
322323
message: `${label} does not satisfy its ActivityPub JSON-LD schema: ${

0 commit comments

Comments
 (0)