While moving Hollo's FEP-044f quote handling onto @fedify/interaction-controls (fedify-dev/hollo#635, fedify-dev/hollo#641), most of the code Hollo had to add was not policy logic. It was code that works around the helper hiding why a check failed. An inbox handler has to choose between retrying later and giving up for good, and the 2.4.0 helpers often don't give it enough information to make that choice. Hollo ended up with about 100 lines of workarounds, listed below. Each of them would be better solved once in the package.
The package's code on main is the same as in 2.4.0, so all of the line references below still apply.
Request verification swallows fetch errors
verifyRequest() dereferences the target and the instrument with suppressError: true (control.ts L326–L330). A failed fetch therefore comes back as missingObject or missingInstrument, the same result as a request that never had one. A caller can't tell a timeout apart from a malformed request. If it treats the failure as final, it drops a valid request. If it retries, it retries garbage.
Hollo now resolves the instrument itself before calling the helper (inbox.ts L770–L794). It has to work on a copy re-parsed from JSON-LD so the request it echoes back is left untouched, and it uses its own transient-error check. That defeats much of the point of calling verifyRequest().
Suggestion: when the target or instrument is referenced by IRI and fetching it fails, report the existing unverifiable/notDereferenceable failure with the URL and the cause. Keep missingObject and missingInstrument for requests that really lack them.
Failures don't say whether retrying could help
verifyAuthorization() reports every fetch problem as unverifiable, but callers still need to classify the cause themselves. A 503, a DNS failure, a 404, and a URL blocked by SSRF protection all look alike. On top of that, materialize() passes the same loader as the context loader, so a failed JSON-LD context fetch surfaces as invalidJsonLd (control.ts L781). That is indistinguishable from a document that is actually malformed.
Hollo wraps the document loader to record every error it throws, then classifies those errors: network errors, UrlError with reason: "dns", 5xx, 408, and 429 count as transient (quote.ts L82–L150). Every Fedify application that verifies authorizations from an inbox needs this same logic.
Suggestion: add a transient: boolean (or retryable) flag to unverifiable failures in both verifyRequest() and verifyAuthorization(), computed from the loader error. Report a failed context fetch as notDereferenceable with the context URL instead of invalidJsonLd. The classification could live in @fedify/vocab-runtime next to FetchError and UrlError, so other packages can reuse it.
Errors from matchesApprovalCollection are swallowed
evaluatePolicy() catches anything the matchesApprovalCollection callback throws and turns it into a denied decision with an unverifiableCollection reason (control.ts L984–L990). The callback is usually a database query. If that query fails for a moment, an application that just follows the decision sends a permanent Reject for a request it would have approved.
Hollo checks for that reason and rethrows (inbox.ts L745–L755), but by then the original error is gone.
Suggestion: put the caught error in the reason as cause, and add an option to let callback errors propagate instead of being converted into a denial.
Constructors are stricter than the wire formats they replace
createRevocation() always reduces the authorization to its IRI (control.ts L182–L188). Hollo has always sent the Delete with the QuoteAuthorization embedded, so it can't use the helper without changing its wire format, and it still builds the Delete by hand.
The constructors also require id and to for Accept, Reject, and Delete. For activities passed straight to sendActivity(), Fedify already assigns an ID when it is missing, and the recipients are given separately. Hollo had to copy Fedify's /#Accept/<uuid> ID shape and add a to it didn't send before.
Suggestion: embed the authorization in createRevocation() when the caller passes an object rather than a URL, as createAccept() already does for result. Make id, to, and cc optional in these constructors, matching createRequest().
Compatibility
All four changes can stay backward compatible. The new failure fields and options are additive. The only output that changes is createRevocation() given an object, and an option can gate that if it seems risky. Once these land, Hollo can drop its own instrument resolution, its loader wrapper and error classification, and its rethrow after evaluatePolicy().
While moving Hollo's FEP-044f quote handling onto @fedify/interaction-controls (fedify-dev/hollo#635, fedify-dev/hollo#641), most of the code Hollo had to add was not policy logic. It was code that works around the helper hiding why a check failed. An inbox handler has to choose between retrying later and giving up for good, and the 2.4.0 helpers often don't give it enough information to make that choice. Hollo ended up with about 100 lines of workarounds, listed below. Each of them would be better solved once in the package.
The package's code on
mainis the same as in 2.4.0, so all of the line references below still apply.Request verification swallows fetch errors
verifyRequest()dereferences the target and the instrument withsuppressError: true(control.ts L326–L330). A failed fetch therefore comes back asmissingObjectormissingInstrument, the same result as a request that never had one. A caller can't tell a timeout apart from a malformed request. If it treats the failure as final, it drops a valid request. If it retries, it retries garbage.Hollo now resolves the instrument itself before calling the helper (inbox.ts L770–L794). It has to work on a copy re-parsed from JSON-LD so the request it echoes back is left untouched, and it uses its own transient-error check. That defeats much of the point of calling
verifyRequest().Suggestion: when the target or instrument is referenced by IRI and fetching it fails, report the existing
unverifiable/notDereferenceablefailure with the URL and thecause. KeepmissingObjectandmissingInstrumentfor requests that really lack them.Failures don't say whether retrying could help
verifyAuthorization()reports every fetch problem asunverifiable, but callers still need to classify thecausethemselves. A 503, a DNS failure, a 404, and a URL blocked by SSRF protection all look alike. On top of that,materialize()passes the same loader as the context loader, so a failed JSON-LD context fetch surfaces asinvalidJsonLd(control.ts L781). That is indistinguishable from a document that is actually malformed.Hollo wraps the document loader to record every error it throws, then classifies those errors: network errors,
UrlErrorwithreason: "dns", 5xx, 408, and 429 count as transient (quote.ts L82–L150). Every Fedify application that verifies authorizations from an inbox needs this same logic.Suggestion: add a
transient: boolean(orretryable) flag tounverifiablefailures in bothverifyRequest()andverifyAuthorization(), computed from the loader error. Report a failed context fetch asnotDereferenceablewith the context URL instead ofinvalidJsonLd. The classification could live in @fedify/vocab-runtime next toFetchErrorandUrlError, so other packages can reuse it.Errors from
matchesApprovalCollectionare swallowedevaluatePolicy()catches anything thematchesApprovalCollectioncallback throws and turns it into adenieddecision with anunverifiableCollectionreason (control.ts L984–L990). The callback is usually a database query. If that query fails for a moment, an application that just follows the decision sends a permanentRejectfor a request it would have approved.Hollo checks for that reason and rethrows (inbox.ts L745–L755), but by then the original error is gone.
Suggestion: put the caught error in the reason as
cause, and add an option to let callback errors propagate instead of being converted into a denial.Constructors are stricter than the wire formats they replace
createRevocation()always reduces the authorization to its IRI (control.ts L182–L188). Hollo has always sent theDeletewith theQuoteAuthorizationembedded, so it can't use the helper without changing its wire format, and it still builds theDeleteby hand.The constructors also require
idandtoforAccept,Reject, andDelete. For activities passed straight tosendActivity(), Fedify already assigns an ID when it is missing, and the recipients are given separately. Hollo had to copy Fedify's/#Accept/<uuid>ID shape and add atoit didn't send before.Suggestion: embed the authorization in
createRevocation()when the caller passes an object rather than aURL, ascreateAccept()already does forresult. Makeid,to, andccoptional in these constructors, matchingcreateRequest().Compatibility
All four changes can stay backward compatible. The new failure fields and options are additive. The only output that changes is
createRevocation()given an object, and an option can gate that if it seems risky. Once these land, Hollo can drop its own instrument resolution, its loader wrapper and error classification, and its rethrow afterevaluatePolicy().