Shim the app-registrations verify for the pod provider drift: satisfiesAccessNeed lives on DataGrants

This commit is contained in:
Sylvain Duchesne
2026-10-12 11:23:45 +02:00
parent 77d567b445
commit 6b48b24350
@@ -0,0 +1,106 @@
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 };
};
}
});
}
};