const { MIME_TYPES } = require('@semapps/mime-types'); // Version-skew compatibility shim for the app registration handshake. // // `@activitypods/app` 2.3.2 (app side) and the local pod provider (HEAD) disagree on where // an AccessGrant records the access needs it satisfies. The app-side `app-registrations.verify` // tests `accessGrant['interop:satisfiesAccessNeed']` directly on the AccessGrant — the umbrella // grant the pod lists in `interop:hasAccessGrant`. The pod provider now writes // `interop:satisfiesAccessNeed` on the DataGrants instead, reached through // AccessGrant -> `interop:hasDataGrant` -> DataGrant. The original test therefore never matches, // verify returns `accessNeedsSatisfied: false`, the inbox processor throws // "One or more required access needs have not been granted", the local AppRegistration is never // stored, and the consent flow loops back to `/login?register_app=true`. // // This shim reimplements the original `verify` verbatim, with the access-need test resolving // through DataGrants (falling back to the grant itself when it carries no DataGrant, so it // tolerates both the old and the new pod-side shape). It is installed as a broker middleware // because the target action lives inside the `@activitypods/app` package: the service itself is // registered by that package, so the handler cannot be overridden by a normal mixin. The // middleware is added to `broker.middlewares` in `created()`, before the services that expose // `app-registrations.verify` are constructed, so it wraps every local action; only // `app-registrations.verify` is intercepted. // // This is a workaround for platform drift, not a Festipod concern: the proper fix belongs // upstream in `@activitypods/app`, and is reported to the provider rather than worked around there. const arrayOf = value => (value === undefined || value === null ? [] : Array.isArray(value) ? value : [value]); module.exports = { name: 'apods-verify-compat', created() { this.broker.middlewares.add({ name: 'ApodsVerifyCompat', localAction(next, action) { if (action.name !== 'app-registrations.verify') return next; return async ctx => { const { appRegistrationUri } = ctx.params; // Use local context to get all data const jsonContext = await ctx.call('jsonld.context.get'); // Get from remote server, not local cache, to get latest version const appRegistration = await ctx.call('ldp.remote.getNetwork', { resourceUri: appRegistrationUri, jsonContext }); const accessGrants = await Promise.all( arrayOf(appRegistration['interop:hasAccessGrant']).map(grantUri => ctx.call('ldp.remote.get', { resourceUri: grantUri, jsonContext, accept: MIME_TYPES.JSON }) ) ); // Resolve each grant's DataGrants, where the pod provider now records the // satisfied access needs (AccessGrant -> interop:hasDataGrant -> DataGrant). const grantsWithDataGrants = await Promise.all( accessGrants.map(async grant => ({ grant, dataGrants: await Promise.all( arrayOf(grant['interop:hasDataGrant']).map(dataGrantUri => ctx.call('ldp.remote.get', { resourceUri: dataGrantUri, jsonContext, accept: MIME_TYPES.JSON }) ) ) })) ); const satisfiesAccessNeed = accessNeedUri => grantsWithDataGrants.some( ({ grant, dataGrants }) => dataGrants.some(dataGrant => dataGrant['interop:satisfiesAccessNeed'] === accessNeedUri) || grant['interop:satisfiesAccessNeed'] === accessNeedUri ); // Get required access need group(s) const filteredContainer = await ctx.call('access-needs-groups.list', { filters: { 'http://www.w3.org/ns/solid/interop#accessNecessity': 'http://www.w3.org/ns/solid/interop#AccessRequired' }, accept: MIME_TYPES.JSON }); const requiredAccessNeedGroups = arrayOf(filteredContainer['ldp:contains']); // Return true if all access needs and special rights of the required AccessNeedGroup(s) are granted const accessNeedsSatisfied = requiredAccessNeedGroups.every( group => arrayOf(group['interop:hasAccessNeed']).every(accessNeedUri => satisfiesAccessNeed(accessNeedUri)) && arrayOf(group['interop:hasSpecialRights']).every(specialRightUri => appRegistration['apods:hasSpecialRights'].some(sr => sr === specialRightUri) ) ); return { accessNeedsSatisfied, appRegistration, accessGrants }; }; } }); } };