Skip to content

Commit f1bba4d

Browse files
committed
feat(api): align federation objects with konrad notes
1 parent 81e36e1 commit f1bba4d

7 files changed

Lines changed: 465 additions & 36 deletions

File tree

packages/api/README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ Env:
2222
- `DOCKER_GIT_API_PORT` (default: `3334`)
2323
- `DOCKER_GIT_PROJECTS_ROOT` (default: `~/.docker-git`)
2424
- `DOCKER_GIT_API_LOG_LEVEL` (default: `info`)
25+
- `DOCKER_GIT_FEDERATION_PUBLIC_ORIGIN` (optional public ActivityPub domain, e.g. `https://social.my-domain.tld`)
26+
- `DOCKER_GIT_FEDERATION_ACTOR` (default: `docker-git`)
2527

2628
## Endpoints (v1)
2729

2830
- `GET /v1/health`
2931
- `POST /v1/federation/inbox` (ForgeFed `Ticket` / `Offer(Ticket)`, ActivityPub `Accept` / `Reject`)
3032
- `GET /v1/federation/issues`
33+
- `GET /v1/federation/actor` (ActivityPub `Person`)
34+
- `GET /v1/federation/outbox`
35+
- `GET /v1/federation/followers`
36+
- `GET /v1/federation/following`
37+
- `GET /v1/federation/liked`
3138
- `POST /v1/federation/follows` (create ActivityPub `Follow` activity for task-feed subscription)
3239
- `GET /v1/federation/follows`
3340
- `GET /v1/projects`
@@ -54,11 +61,13 @@ curl -s http://localhost:3334/v1/projects
5461
curl -s -X POST http://localhost:3334/v1/projects/<projectId>/up
5562
curl -s -N http://localhost:3334/v1/projects/<projectId>/events
5663

64+
curl -s http://localhost:3334/v1/federation/actor
65+
5766
curl -s -X POST http://localhost:3334/v1/federation/follows \
5867
-H 'content-type: application/json' \
59-
-d '{"actor":"https://dev.example/users/bot","object":"https://tracker.example/issues/followers"}'
68+
-d '{"domain":"social.my-domain.tld","object":"https://social.my-domain.tld/issues/followers"}'
6069

6170
curl -s -X POST http://localhost:3334/v1/federation/inbox \
6271
-H 'content-type: application/json' \
63-
-d '{"type":"Offer","target":"https://tracker.example/issues","object":{"type":"Ticket","id":"https://tracker.example/issues/42","attributedTo":"https://origin.example/users/alice","summary":"Title","content":"Body"}}'
72+
-d '{"@context":["https://www.w3.org/ns/activitystreams","https://forgefed.org/ns"],"id":"https://social.my-domain.tld/offers/42","type":"Offer","target":"https://social.my-domain.tld/issues","object":{"type":"Ticket","id":"https://social.my-domain.tld/issues/42","attributedTo":"https://origin.my-domain.tld/users/alice","summary":"Title","content":"Body"}}'
6473
```

packages/api/src/api/contracts.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ export type FederationIssueRecord = {
126126
}
127127

128128
export type CreateFollowRequest = {
129-
readonly actor: string
129+
readonly actor?: string | undefined
130130
readonly object: string
131+
readonly domain?: string | undefined
131132
readonly inbox?: string | undefined
132133
readonly to?: ReadonlyArray<string> | undefined
133134
readonly capability?: string | undefined
@@ -145,6 +146,28 @@ export type ActivityPubFollowActivity = {
145146
readonly capability?: string | undefined
146147
}
147148

149+
export type ActivityPubPerson = {
150+
readonly "@context": "https://www.w3.org/ns/activitystreams"
151+
readonly type: "Person"
152+
readonly id: string
153+
readonly name: string
154+
readonly preferredUsername: string
155+
readonly summary: string
156+
readonly inbox: string
157+
readonly outbox: string
158+
readonly followers: string
159+
readonly following: string
160+
readonly liked: string
161+
}
162+
163+
export type ActivityPubOrderedCollection = {
164+
readonly "@context": "https://www.w3.org/ns/activitystreams"
165+
readonly type: "OrderedCollection"
166+
readonly id: string
167+
readonly totalItems: number
168+
readonly orderedItems: ReadonlyArray<unknown>
169+
}
170+
148171
export type FollowSubscription = {
149172
readonly id: string
150173
readonly activityId: string

packages/api/src/api/schema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ export const CreateAgentRequestSchema = Schema.Struct({
4848
})
4949

5050
export const CreateFollowRequestSchema = Schema.Struct({
51-
actor: Schema.String,
51+
actor: OptionalString,
5252
object: Schema.String,
53+
domain: OptionalString,
5354
inbox: OptionalString,
5455
to: Schema.optional(Schema.Array(Schema.String)),
5556
capability: OptionalString

packages/api/src/http.ts

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ import {
1818
createFollowSubscription,
1919
ingestFederationInbox,
2020
listFederationIssues,
21-
listFollowSubscriptions
21+
listFollowSubscriptions,
22+
makeFederationActorDocument,
23+
makeFederationContext,
24+
makeFederationFollowersCollection,
25+
makeFederationFollowingCollection,
26+
makeFederationLikedCollection,
27+
makeFederationOutboxCollection
2228
} from "./services/federation.js"
2329
import {
2430
createProjectFromRequest,
@@ -117,6 +123,55 @@ const readCreateProjectRequest = () => HttpServerRequest.schemaBodyJson(CreatePr
117123
const readCreateFollowRequest = () => HttpServerRequest.schemaBodyJson(CreateFollowRequestSchema)
118124
const readInboxPayload = () => HttpServerRequest.schemaBodyJson(Schema.Unknown)
119125

126+
const configuredFederationPublicOrigin =
127+
process.env["DOCKER_GIT_FEDERATION_PUBLIC_ORIGIN"] ??
128+
process.env["DOCKER_GIT_API_PUBLIC_URL"]
129+
130+
const configuredFederationActorUsername =
131+
process.env["DOCKER_GIT_FEDERATION_ACTOR"] ?? "docker-git"
132+
133+
const readHeader = (
134+
request: HttpServerRequest.HttpServerRequest,
135+
key: string
136+
): string | undefined => {
137+
const value = request.headers[key.toLowerCase()]
138+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined
139+
}
140+
141+
const firstCommaValue = (value: string | undefined): string | undefined => {
142+
if (value === undefined) {
143+
return undefined
144+
}
145+
const first = value.split(",")[0]?.trim()
146+
return first && first.length > 0 ? first : undefined
147+
}
148+
149+
const resolveRequestOrigin = (request: HttpServerRequest.HttpServerRequest): string => {
150+
const forwardedHost = firstCommaValue(readHeader(request, "x-forwarded-host"))
151+
const host = forwardedHost ?? readHeader(request, "host")
152+
const proto = firstCommaValue(readHeader(request, "x-forwarded-proto")) ?? "http"
153+
if (host === undefined || host.length === 0) {
154+
return "http://localhost:3334"
155+
}
156+
return `${proto}://${host}`
157+
}
158+
159+
const resolveFederationContext = (
160+
request: HttpServerRequest.HttpServerRequest,
161+
requestedDomain?: string | undefined
162+
) => {
163+
const fromBody = requestedDomain?.trim()
164+
const publicOrigin =
165+
fromBody && fromBody.length > 0
166+
? fromBody
167+
: configuredFederationPublicOrigin ?? resolveRequestOrigin(request)
168+
169+
return makeFederationContext({
170+
publicOrigin,
171+
actorUsername: configuredFederationActorUsername
172+
})
173+
}
174+
120175
export const makeRouter = () => {
121176
const base = HttpRouter.empty.pipe(
122177
HttpRouter.get("/", textResponse(uiHtml, "text/html; charset=utf-8", 200)),
@@ -130,11 +185,53 @@ export const makeRouter = () => {
130185
Effect.catchAll(errorResponse)
131186
)
132187
),
188+
HttpRouter.get(
189+
"/v1/federation/actor",
190+
Effect.gen(function*(_) {
191+
const request = yield* _(HttpServerRequest.HttpServerRequest)
192+
const context = yield* _(resolveFederationContext(request))
193+
return yield* _(jsonResponse(makeFederationActorDocument(context), 200))
194+
}).pipe(Effect.catchAll(errorResponse))
195+
),
196+
HttpRouter.get(
197+
"/v1/federation/outbox",
198+
Effect.gen(function*(_) {
199+
const request = yield* _(HttpServerRequest.HttpServerRequest)
200+
const context = yield* _(resolveFederationContext(request))
201+
return yield* _(jsonResponse(makeFederationOutboxCollection(context), 200))
202+
}).pipe(Effect.catchAll(errorResponse))
203+
),
204+
HttpRouter.get(
205+
"/v1/federation/followers",
206+
Effect.gen(function*(_) {
207+
const request = yield* _(HttpServerRequest.HttpServerRequest)
208+
const context = yield* _(resolveFederationContext(request))
209+
return yield* _(jsonResponse(makeFederationFollowersCollection(context), 200))
210+
}).pipe(Effect.catchAll(errorResponse))
211+
),
212+
HttpRouter.get(
213+
"/v1/federation/following",
214+
Effect.gen(function*(_) {
215+
const request = yield* _(HttpServerRequest.HttpServerRequest)
216+
const context = yield* _(resolveFederationContext(request))
217+
return yield* _(jsonResponse(makeFederationFollowingCollection(context), 200))
218+
}).pipe(Effect.catchAll(errorResponse))
219+
),
220+
HttpRouter.get(
221+
"/v1/federation/liked",
222+
Effect.gen(function*(_) {
223+
const request = yield* _(HttpServerRequest.HttpServerRequest)
224+
const context = yield* _(resolveFederationContext(request))
225+
return yield* _(jsonResponse(makeFederationLikedCollection(context), 200))
226+
}).pipe(Effect.catchAll(errorResponse))
227+
),
133228
HttpRouter.post(
134229
"/v1/federation/follows",
135230
Effect.gen(function*(_) {
136-
const request = yield* _(readCreateFollowRequest())
137-
const created = yield* _(createFollowSubscription(request))
231+
const requestBody = yield* _(readCreateFollowRequest())
232+
const request = yield* _(HttpServerRequest.HttpServerRequest)
233+
const context = yield* _(resolveFederationContext(request, requestBody.domain))
234+
const created = yield* _(createFollowSubscription(requestBody, context))
138235
return yield* _(jsonResponse(created, 201))
139236
}).pipe(Effect.catchAll(errorResponse))
140237
),
@@ -152,7 +249,10 @@ export const makeRouter = () => {
152249
const result = yield* _(ingestFederationInbox(payload))
153250
return yield* _(jsonResponse({ result }, 202))
154251
}).pipe(Effect.catchAll(errorResponse))
155-
),
252+
)
253+
)
254+
255+
const withProjects = base.pipe(
156256
HttpRouter.get(
157257
"/v1/projects",
158258
listProjects().pipe(
@@ -226,7 +326,7 @@ export const makeRouter = () => {
226326
)
227327
)
228328

229-
const withAgents = base.pipe(
329+
const withAgents = withProjects.pipe(
230330
HttpRouter.post(
231331
"/v1/projects/:projectId/agents",
232332
Effect.gen(function*(_) {

0 commit comments

Comments
 (0)