Skip to content

Commit 460deb3

Browse files
authored
fix(api): use Fedify for ActivityPub protocol documents (#337)
* fix(api): use Fedify for ActivityPub protocol documents * test(api): strengthen Fedify protocol invariants * docs(api): document Fedify WebFinger proof * test(api): keep federation properties in Effect * fix(app): expose WebFinger at browser root
1 parent 9408763 commit 460deb3

16 files changed

Lines changed: 995 additions & 1056 deletions

bun.lock

Lines changed: 83 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/api/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Optional env:
8282

8383
- `GET /health`
8484
- `POST /federation/inbox` (ForgeFed `Ticket` / `Offer(Ticket)`, ActivityPub `Accept` / `Reject`)
85+
- `GET /.well-known/webfinger` (Fedify WebFinger document for the local federation actor)
8586
- `GET /federation/issues`
8687
- `GET /federation/actor` (ActivityPub `Person`)
8788
- `GET /federation/outbox`
@@ -116,6 +117,14 @@ Optional env:
116117

117118
Exchange targets must be explicit. Use `https://exchange.lefine.pro`, an actor URL, or a handle like `code@exchange.lefine.pro`; the API resolves the code actor document, stores its `inbox/outbox/followers/publicKey`, sends `Follow`, and polls the stored `outbox`.
118119

120+
Local ActivityPub documents are serialized with Fedify and use only the supported ActivityStreams and security JSON-LD contexts. Mastodon-specific extension contexts and keys such as `https://purl.archive.org/socialweb/webfinger`, `toot`, `featured`, `featuredTags`, `alsoKnownAs`, `movedTo`, and `interactionPolicy` are not emitted by docker-git.
121+
122+
The local actor is discoverable through WebFinger:
123+
124+
```bash
125+
./ctl request GET '/.well-known/webfinger?resource=acct:docker-git@social.provercoder.ai'
126+
```
127+
119128
```bash
120129
./ctl request POST /federation/exchange/subscriptions '{
121130
"domain":"https://social.provercoder.ai",

packages/api/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"@effect/platform": "^0.96.1",
2424
"@effect/platform-node": "^0.106.0",
2525
"@effect/schema": "^0.75.5",
26+
"@fedify/fedify": "^2.2.3",
27+
"@fedify/vocab": "^2.2.3",
2628
"effect": "^3.21.2",
2729
"node-pty": "^1.1.0",
2830
"ws": "^8.20.1"
Lines changed: 1 addition & 222 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
11
import * as Schema from "effect/Schema"
2-
import type * as SchemaAST from "effect/SchemaAST"
32

43
import {
54
activityStreamsJsonLdContext,
6-
forgeFedJsonLdContext,
7-
securityJsonLdContext,
8-
socialWebWebfingerJsonLdContext
5+
forgeFedJsonLdContext
96
} from "./contracts.js"
107

118
export type JsonPrimitive = boolean | number | string | null
129
export type JsonValue = JsonPrimitive | JsonObject | ReadonlyArray<JsonValue>
1310
export type JsonObject = Readonly<{ [key: string]: JsonValue }>
1411

15-
export const exactActivityPubParseOptions: SchemaAST.ParseOptions = {
16-
onExcessProperty: "error"
17-
}
18-
1912
export const JsonValueSchema: Schema.Schema<JsonValue> = Schema.suspend(() =>
2013
Schema.Union(
2114
Schema.Null,
@@ -28,67 +21,12 @@ export const JsonValueSchema: Schema.Schema<JsonValue> = Schema.suspend(() =>
2821
)
2922

3023
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-
})
3724

3825
export const ActivityForgeFedJsonLdContextSchema = Schema.Tuple(
3926
Schema.Literal(activityStreamsJsonLdContext),
4027
Schema.Literal(forgeFedJsonLdContext)
4128
)
4229

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-
9230
export const ForgeFedTicketSourceSchema = Schema.Struct({
9331
content: OptionalString,
9432
mediaType: OptionalString
@@ -110,118 +48,6 @@ export const ForgeFedTicketSchema = Schema.Struct({
11048
raw: Schema.optional(JsonValueSchema)
11149
})
11250

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-
22551
export const ActivityPubFollowActivitySchema = Schema.Struct({
22652
"@context": ActivityForgeFedJsonLdContextSchema,
22753
id: Schema.String,
@@ -239,50 +65,3 @@ export const LocalActivityPubOrderedCollectionSchema = Schema.Struct({
23965
totalItems: Schema.Number,
24066
orderedItems: Schema.Array(JsonValueSchema)
24167
})
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-
)

packages/api/src/api/contracts.ts

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,9 @@ import type * as Schema from "effect/Schema"
33
import type {
44
ActivityForgeFedJsonLdContextSchema,
55
ActivityPubFollowActivitySchema,
6-
ActivityPubOrderedCollectionPageSchema,
7-
ActivityPubOrderedCollectionSchema,
8-
ActivityPubPersonSchema,
9-
ActivityPubPublicKeySchema,
10-
ActorJsonLdContextSchema,
116
ForgeFedTicketSchema,
127
ForgeFedTicketSourceSchema,
13-
LocalActivityPubFollowersCollectionSchema,
14-
LocalActivityPubOrderedCollectionPageSchema,
15-
LocalActivityPubOrderedCollectionSchema,
16-
LocalActivityPubPersonSchema
8+
LocalActivityPubOrderedCollectionSchema
179
} from "./activitypub-schema.js"
1810

1911
export type ProjectStatus = "running" | "stopped" | "unknown"
@@ -560,23 +552,20 @@ export type ContainerTaskSnapshot = {
560552
export const activityStreamsJsonLdContext = "https://www.w3.org/ns/activitystreams" as const
561553
export const forgeFedJsonLdContext = "https://forgefed.org/ns" as const
562554
export const securityJsonLdContext = "https://w3id.org/security/v1" as const
563-
export const socialWebWebfingerJsonLdContext = "https://purl.archive.org/socialweb/webfinger" as const
564555
export const activityForgeFedJsonLdContext = [
565556
activityStreamsJsonLdContext,
566557
forgeFedJsonLdContext
567558
] as const
568559
export const actorJsonLdContext = [
569560
activityStreamsJsonLdContext,
570-
securityJsonLdContext,
571-
forgeFedJsonLdContext
561+
securityJsonLdContext
572562
] as const
573563
export const federationJsonLdContentType =
574564
`application/ld+json; profile="${activityStreamsJsonLdContext}"` as const
575565
export const federationJsonLdResponseContentType =
576566
`${federationJsonLdContentType}; charset=utf-8` as const
577567

578568
export type ActivityForgeFedJsonLdContext = Schema.Schema.Type<typeof ActivityForgeFedJsonLdContextSchema>
579-
export type ActorJsonLdContext = Schema.Schema.Type<typeof ActorJsonLdContextSchema>
580569

581570
export type ForgeFedTicket = Schema.Schema.Type<typeof ForgeFedTicketSchema>
582571

@@ -622,22 +611,7 @@ export type FollowStatus = "pending" | "accepted" | "rejected"
622611

623612
export type ActivityPubFollowActivity = Schema.Schema.Type<typeof ActivityPubFollowActivitySchema>
624613

625-
export type ActivityPubPublicKey = Schema.Schema.Type<typeof ActivityPubPublicKeySchema>
626-
627-
export type ActivityPubPerson = Schema.Schema.Type<typeof ActivityPubPersonSchema>
628-
629-
export type LocalActivityPubPerson = Schema.Schema.Type<typeof LocalActivityPubPersonSchema>
630-
631-
export type ActivityPubOrderedCollection = Schema.Schema.Type<typeof ActivityPubOrderedCollectionSchema>
632-
633-
export type LocalActivityPubOrderedCollection =
634-
| Schema.Schema.Type<typeof LocalActivityPubOrderedCollectionSchema>
635-
| Schema.Schema.Type<typeof LocalActivityPubFollowersCollectionSchema>
636-
637-
export type ActivityPubOrderedCollectionPage = Schema.Schema.Type<typeof ActivityPubOrderedCollectionPageSchema>
638-
639-
export type LocalActivityPubOrderedCollectionPage =
640-
Schema.Schema.Type<typeof LocalActivityPubOrderedCollectionPageSchema>
614+
export type LocalActivityPubOrderedCollection = Schema.Schema.Type<typeof LocalActivityPubOrderedCollectionSchema>
641615

642616
export type FollowSubscription = {
643617
readonly id: string

packages/api/src/api/schema.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,10 @@ import * as Schema from "effect/Schema"
33
export {
44
ActivityForgeFedJsonLdContextSchema,
55
ActivityPubFollowActivitySchema,
6-
ActivityPubOrderedCollectionPageSchema,
7-
ActivityPubOrderedCollectionSchema,
8-
ActivityPubPersonSchema,
9-
ActivityPubPublicKeySchema,
10-
ActorJsonLdContextSchema,
116
ForgeFedTicketSchema,
127
ForgeFedTicketSourceSchema,
13-
JsonLdContextSchema,
148
JsonValueSchema,
15-
LocalActivityPubFollowersCollectionSchema,
16-
LocalActivityPubOrderedCollectionPageSchema,
17-
LocalActivityPubOrderedCollectionSchema,
18-
LocalActivityPubPersonSchema,
19-
MastodonFollowersOrderedCollectionPageSchema,
20-
MastodonFollowersOrderedCollectionSchema,
21-
MastodonIssueActivityPubPersonSchema,
22-
exactActivityPubParseOptions
9+
LocalActivityPubOrderedCollectionSchema
2310
} from "./activitypub-schema.js"
2411

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

0 commit comments

Comments
 (0)