Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/ap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export async function activityPubPlugin(fastify, options = {}) {
const getActorId = (request) => {
const protocol = getProtocol(request)
const host = request.headers['x-forwarded-host'] || request.hostname
return `${protocol}://${host}/profile/card#me`
return `${protocol}://${host}/profile/card.jsonld#me`
}

// Helper to get base URL
Expand Down Expand Up @@ -96,11 +96,11 @@ export async function activityPubPlugin(fastify, options = {}) {
return reply.code(404).send({ error: 'Not found' })
}

// For now, accept any username and map to /profile/card#me
// For now, accept any username and map to /profile/card.jsonld#me
// In multi-user mode, we'd look up the user
const baseUrl = getBaseUrl(request)
const actorUrl = `${baseUrl}/profile/card#me`
const profileUrl = `${baseUrl}/profile/card`
const actorUrl = `${baseUrl}/profile/card.jsonld#me`
const profileUrl = `${baseUrl}/profile/card.jsonld`

const response = webfinger.createResponse(
`${parsed.username}@${parsed.domain}`,
Expand Down Expand Up @@ -174,18 +174,18 @@ export async function activityPubPlugin(fastify, options = {}) {
// Inbox endpoint
const inboxHandler = createInboxHandler(config, keypair)
fastify.post('/inbox', inboxHandler)
fastify.post('/profile/card/inbox', inboxHandler)
fastify.post('/profile/card.jsonld/inbox', inboxHandler)

// Outbox endpoint
const outboxHandler = createOutboxHandler(config, keypair)
const outboxPostHandler = createOutboxPostHandler(config, keypair)
fastify.get('/profile/card/outbox', outboxHandler)
fastify.post('/profile/card/outbox', outboxPostHandler)
fastify.get('/profile/card.jsonld/outbox', outboxHandler)
fastify.post('/profile/card.jsonld/outbox', outboxPostHandler)

// Followers/Following collections
const collectionsHandler = createCollectionsHandler(config)
fastify.get('/profile/card/followers', (req, reply) => collectionsHandler(req, reply, 'followers'))
fastify.get('/profile/card/following', (req, reply) => collectionsHandler(req, reply, 'following'))
fastify.get('/profile/card.jsonld/followers', (req, reply) => collectionsHandler(req, reply, 'followers'))
fastify.get('/profile/card.jsonld/following', (req, reply) => collectionsHandler(req, reply, 'following'))

// Mastodon-compatible API endpoints
fastify.post('/api/v1/apps', createAppsHandler())
Expand Down
4 changes: 2 additions & 2 deletions src/ap/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ export function loadOrCreateKeypair(path = DEFAULT_KEY_PATH) {

/**
* Get key ID for HTTP Signatures
* @param {string} actorId - Actor URL (e.g., https://example.com/profile/card#me)
* @returns {string} Key ID (e.g., https://example.com/profile/card#main-key)
* @param {string} actorId - Actor URL (e.g., https://example.com/profile/card.jsonld#me)
* @returns {string} Key ID (e.g., https://example.com/profile/card.jsonld#main-key)
*/
export function getKeyId(actorId) {
// Strip fragment and add #main-key
Expand Down
2 changes: 1 addition & 1 deletion src/ap/routes/actor.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function createActorHandler(config, keypair) {
}
protocol = protocol || request.protocol
const baseUrl = `${protocol}://${host}`
const profileUrl = `${baseUrl}/profile/card`
const profileUrl = `${baseUrl}/profile/card.jsonld`
const actorId = `${profileUrl}#me`

const actor = {
Expand Down
2 changes: 1 addition & 1 deletion src/ap/routes/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function createCollectionsHandler(config) {
const protocol = request.headers['x-forwarded-proto'] || request.protocol
const host = request.headers['x-forwarded-host'] || request.hostname
const baseUrl = `${protocol}://${host}`
const profileUrl = `${baseUrl}/profile/card`
const profileUrl = `${baseUrl}/profile/card.jsonld`

let items, totalItems

Expand Down
2 changes: 1 addition & 1 deletion src/ap/routes/inbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export function createInboxHandler(config, keypair) {
const protocol = request.headers['x-forwarded-proto'] || request.protocol
const host = request.headers['x-forwarded-host'] || request.hostname
const baseUrl = `${protocol}://${host}`
const profileUrl = `${baseUrl}/profile/card`
const profileUrl = `${baseUrl}/profile/card.jsonld`
const actorId = `${profileUrl}#me`

request.log.info(`Received ${activity.type} from ${activity.actor}`)
Expand Down
4 changes: 2 additions & 2 deletions src/ap/routes/mastodon.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ export function createVerifyCredentialsHandler (config) {
acct: config.username,
display_name: config.displayName,
note: config.summary ? `<p>${escapeHtml(config.summary)}</p>` : '',
url: `${baseUrl}/profile/card`,
uri: `${baseUrl}/profile/card#me`,
url: `${baseUrl}/profile/card.jsonld`,
uri: `${baseUrl}/profile/card.jsonld#me`,
avatar: `${baseUrl}/profile/avatar.png`,
header: '',
locked: false,
Expand Down
4 changes: 2 additions & 2 deletions src/ap/routes/outbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function createOutboxHandler(config, keypair) {
const protocol = request.headers['x-forwarded-proto'] || request.protocol
const host = request.headers['x-forwarded-host'] || request.hostname
const baseUrl = `${protocol}://${host}`
const profileUrl = `${baseUrl}/profile/card`
const profileUrl = `${baseUrl}/profile/card.jsonld`
const actorId = `${profileUrl}#me`

const posts = getPosts(20)
Expand Down Expand Up @@ -63,7 +63,7 @@ export function createOutboxPostHandler(config, keypair) {
const protocol = request.headers['x-forwarded-proto'] || request.protocol
const host = request.headers['x-forwarded-host'] || request.hostname
const baseUrl = `${protocol}://${host}`
const profileUrl = `${baseUrl}/profile/card`
const profileUrl = `${baseUrl}/profile/card.jsonld`
const actorId = `${profileUrl}#me`

// Parse body
Expand Down
4 changes: 2 additions & 2 deletions src/db/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ export async function dbPlugin(fastify, options) {
if (podName) {
// Build expected WebID for both path and subdomain modes
const expectedWebId = request.subdomainsEnabled && request.baseDomain
? `${request.protocol}://${podName}.${request.baseDomain}/profile/card#me`
: `${request.protocol}://${request.hostname}/${podName}/profile/card#me`;
? `${request.protocol}://${podName}.${request.baseDomain}/profile/card.jsonld#me`
: `${request.protocol}://${request.hostname}/${podName}/profile/card.jsonld#me`;
if (webId !== expectedWebId) {
return reply.code(403).send({ error: 'Forbidden', message: 'You can only write to your own /db/ space' });
}
Expand Down
45 changes: 22 additions & 23 deletions src/handlers/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,20 +173,20 @@ export async function createPodStructure(name, webId, podUri, issuer, defaultQuo
await storage.createContainer(`${podPath}settings/`);
await storage.createContainer(`${podPath}profile/`);

// Generate and write WebID profile at /profile/card (standard Solid location)
const profileHtml = generateProfile({ webId, name, podUri, issuer });
await storage.write(`${podPath}profile/card`, profileHtml);
// Generate and write WebID profile at /profile/card.jsonld
const profile = generateProfile({ webId, name, podUri, issuer });
await storage.write(`${podPath}profile/card.jsonld`, serialize(profile));

// Generate and write preferences (mashlib-compatible paths)
// Generate and write preferences
const prefs = generatePreferences({ webId, podUri });
await storage.write(`${podPath}settings/Preferences.ttl`, serialize(prefs));
await storage.write(`${podPath}settings/prefs.jsonld`, serialize(prefs));

// Generate and write type indexes with .ttl extension for mashlib
const publicTypeIndex = generateTypeIndex(`${podUri}settings/publicTypeIndex.ttl`);
await storage.write(`${podPath}settings/publicTypeIndex.ttl`, serialize(publicTypeIndex));
// Generate and write type indexes
const publicTypeIndex = generateTypeIndex(`${podUri}settings/publicTypeIndex.jsonld`);
await storage.write(`${podPath}settings/publicTypeIndex.jsonld`, serialize(publicTypeIndex));

const privateTypeIndex = generateTypeIndex(`${podUri}settings/privateTypeIndex.ttl`);
await storage.write(`${podPath}settings/privateTypeIndex.ttl`, serialize(privateTypeIndex));
const privateTypeIndex = generateTypeIndex(`${podUri}settings/privateTypeIndex.jsonld`);
await storage.write(`${podPath}settings/privateTypeIndex.jsonld`, serialize(privateTypeIndex));

// Create default ACL files
// Pod root: owner full control, public read
Expand Down Expand Up @@ -229,13 +229,13 @@ export async function createPodStructure(name, webId, podUri, issuer, defaultQuo
*
* Creates the following structure:
* /{name}/
* /{name}/profile/card - WebID profile
* /{name}/inbox/ - Notifications
* /{name}/public/ - Public files
* /{name}/private/ - Private files
* /{name}/settings/prefs - Preferences
* /{name}/settings/publicTypeIndex
* /{name}/settings/privateTypeIndex
* /{name}/profile/card.jsonld - WebID profile
* /{name}/inbox/ - Notifications
* /{name}/public/ - Public files
* /{name}/private/ - Private files
* /{name}/settings/prefs.jsonld - Preferences
* /{name}/settings/publicTypeIndex.jsonld
* /{name}/settings/privateTypeIndex.jsonld
*/
export async function handleCreatePod(request, reply) {
// Read-only mode - block pod creation
Expand Down Expand Up @@ -272,23 +272,22 @@ export async function handleCreatePod(request, reply) {
return reply.code(409).send({ error: 'Pod already exists' });
}

// Build URIs
// WebID follows standard Solid convention: /alice/profile/card#me
// Build URIs. WebID is the JSON-LD profile with an #me fragment.
const subdomainsEnabled = request.subdomainsEnabled;
const baseDomain = request.baseDomain;

let baseUri, podUri, webId;
if (subdomainsEnabled && baseDomain) {
// Subdomain mode: alice.example.com/profile/card#me
// Subdomain mode: alice.example.com/profile/card.jsonld#me
const podHost = `${name}.${baseDomain}`;
baseUri = `${request.protocol}://${baseDomain}`;
podUri = `${request.protocol}://${podHost}/`;
webId = `${podUri}profile/card#me`;
webId = `${podUri}profile/card.jsonld#me`;
} else {
// Path mode: example.com/alice/profile/card#me
// Path mode: example.com/alice/profile/card.jsonld#me
baseUri = `${request.protocol}://${request.hostname}`;
podUri = `${baseUri}${podPath}`;
webId = `${podUri}profile/card#me`;
webId = `${podUri}profile/card.jsonld#me`;
}

// Issuer needs trailing slash for CTH compatibility
Expand Down
10 changes: 5 additions & 5 deletions src/idp/interactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,20 +405,20 @@ export async function handleRegisterPost(request, reply, issuer, inviteOnly = fa
}

try {
// Build URLs - WebID follows standard Solid convention: /profile/card#me
// Build URLs. WebID is the JSON-LD profile with an #me fragment.
const subdomainsEnabled = request.subdomainsEnabled;
const baseDomain = request.baseDomain;
const baseUrl = issuer.endsWith('/') ? issuer.slice(0, -1) : issuer;

let podUri, webId;
if (subdomainsEnabled && baseDomain) {
// Subdomain mode: alice.example.com/profile/card#me
// Subdomain mode: alice.example.com/profile/card.jsonld#me
podUri = `${request.protocol}://${username}.${baseDomain}/`;
webId = `${podUri}profile/card#me`;
webId = `${podUri}profile/card.jsonld#me`;
} else {
// Path mode: example.com/alice/profile/card#me
// Path mode: example.com/alice/profile/card.jsonld#me
podUri = `${baseUrl}/${username}/`;
webId = `${podUri}profile/card#me`;
webId = `${podUri}profile/card.jsonld#me`;
}

// Check if pod already exists
Expand Down
32 changes: 18 additions & 14 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,12 +314,12 @@ export function createServer(options = {}) {
// Note: OPTIONS requests are handled by handleOptions to include Accept-* headers
});

// ActivityPub actor endpoint - dedicated route for /profile/card with AP Accept header
// ActivityPub actor endpoint - dedicated route for /profile/card.jsonld with AP Accept header
// Registered before wildcard routes to take priority
if (activitypubEnabled) {
fastify.route({
method: 'GET',
url: '/profile/card',
url: '/profile/card.jsonld',
handler: async (request, reply) => {
const accept = request.headers.accept || '';
const wantsAP = accept.includes('activity+json') ||
Expand Down Expand Up @@ -426,13 +426,13 @@ export function createServer(options = {}) {
fastify.addHook('preHandler', async (request, reply) => {
// Skip auth for pod creation, OPTIONS, IdP routes, mashlib, well-known, notifications, nostr, git, and AP
const mashlibPaths = ['/mashlib.min.js', '/mash.css', '/841.mashlib.min.js'];
const apPaths = ['/inbox', '/profile/card/inbox', '/profile/card/outbox', '/profile/card/followers', '/profile/card/following',
const apPaths = ['/inbox', '/profile/card.jsonld/inbox', '/profile/card.jsonld/outbox', '/profile/card.jsonld/followers', '/profile/card.jsonld/following',
'/api/v1/apps', '/api/v1/instance', '/api/v1/accounts/verify_credentials',
'/oauth/authorize', '/oauth/token'];
// Check if request wants ActivityPub content for profile
const accept = request.headers.accept || '';
const wantsAP = accept.includes('activity+json') || accept.includes('ld+json; profile="https://www.w3.org/ns/activitystreams"');
const isProfileAP = activitypubEnabled && wantsAP && (request.url === '/profile/card' || request.url.startsWith('/profile/card?'));
const isProfileAP = activitypubEnabled && wantsAP && (request.url === '/profile/card.jsonld' || request.url.startsWith('/profile/card.jsonld?'));
Comment on lines 428 to +435

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apPaths now lists /profile/card.jsonld/*, but the ActivityPub plugin currently registers /profile/card/* endpoints (see src/ap/index.js: fastify.post('/profile/card/inbox', ...), etc.). As a result, ActivityPub inbox/outbox/followers/following requests will no longer match this auth-bypass and may start requiring WAC auth unexpectedly. Either update the plugin routes to .jsonld or include both the legacy /profile/card/* and new /profile/card.jsonld/* paths here during transition.

Copilot uses AI. Check for mistakes.
if (request.url === '/.pods' ||
request.url === '/.notifications' ||
request.method === 'OPTIONS' ||
Expand Down Expand Up @@ -557,11 +557,15 @@ export function createServer(options = {}) {
const isRootPod = !singleUserName || singleUserName === '/';
const podPath = isRootPod ? '/' : `/${singleUserName}/`;
const podUri = isRootPod ? `${baseUrl}/` : `${baseUrl}/${singleUserName}/`;
const webId = `${podUri}profile/card#me`;
const webId = `${podUri}profile/card.jsonld#me`;
const displayName = isRootPod ? 'me' : singleUserName;

// Check if pod already exists (profile/card is the indicator)
const profileExists = await storage.exists(`${podPath}profile/card`);
// Check if pod already exists. Accept either the new `card.jsonld`
// or legacy extensionless `card` layout so we don't re-seed a pod
// that was created by an older JSS version.
const profileExists =
await storage.exists(`${podPath}profile/card.jsonld`) ||
await storage.exists(`${podPath}profile/card`);

Comment on lines +560 to 569

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In single-user mode the existence check uses only profile/card.jsonld as the indicator. If an existing pod was created with the old profile/card (or other legacy layout), this will incorrectly treat it as missing and then overwrite/create root pod files (including /.acl, /settings/*, etc.). Consider checking for both profile/card.jsonld and the legacy profile/card (and/or skipping creation if /.acl or /profile/ already exist), or implement an explicit migration path instead of unconditional creation.

Copilot uses AI. Check for mistakes.
if (!profileExists) {
fastify.log.info(`Creating single-user pod at ${podUri}...`);
Expand Down Expand Up @@ -593,18 +597,18 @@ export function createServer(options = {}) {
await storage.createContainer('/profile/');

// Generate profile
const profileHtml = generateProfile({ webId, name: displayName, podUri, issuer });
await storage.write('/profile/card', profileHtml);
const profile = generateProfile({ webId, name: displayName, podUri, issuer });
await storage.write('/profile/card.jsonld', serialize(profile));

// Preferences and type indexes
const prefs = generatePreferences({ webId, podUri });
await storage.write('/settings/Preferences.ttl', serialize(prefs));
await storage.write('/settings/prefs.jsonld', serialize(prefs));

const publicTypeIndex = generateTypeIndex(`${podUri}settings/publicTypeIndex.ttl`);
await storage.write('/settings/publicTypeIndex.ttl', serialize(publicTypeIndex));
const publicTypeIndex = generateTypeIndex(`${podUri}settings/publicTypeIndex.jsonld`);
await storage.write('/settings/publicTypeIndex.jsonld', serialize(publicTypeIndex));

const privateTypeIndex = generateTypeIndex(`${podUri}settings/privateTypeIndex.ttl`);
await storage.write('/settings/privateTypeIndex.ttl', serialize(privateTypeIndex));
const privateTypeIndex = generateTypeIndex(`${podUri}settings/privateTypeIndex.jsonld`);
await storage.write('/settings/privateTypeIndex.jsonld', serialize(privateTypeIndex));

// ACL files
const rootAcl = generateOwnerAcl(podUri, webId, true);
Expand Down
Loading