Skip to content
These docs describe arocapi v5, which is in beta. Install it with pnpm add arocapi@beta.

Licences & authorisation URLs

In a PILARS archive, every item carries a licence: a plain-language statement of who may use it and how, stored with the data. arocapi builds its access model on those licences.

Every entity has two licence ids, usually the URLs of licence documents named in its RO-Crate:

Field Governs Route it guards
metadataLicenseId Reading the entity’s full RO-Crate metadata. /entity/:id/metadata
contentLicenseId Downloading the bytes of a file entity. /file/:id

Splitting them lets a repository publish what an item is while restricting the item itself. A common arrangement is an open licence such as CC BY 4.0 for metadata and a closed licence for the recordings of a sensitive collection.

Both ids are part of the entity’s Record, so listings, search results and /entity/:id show which licences apply even to a user who can’t hold them. Access never hides an entity. Hiding is the job of the visibility filters, which run first. Keep licences out of them, so a user without a licence can still find the Record and the authorisation URL that lets them apply.

Your access function decides, per entity and per request, whether the request may read the Metadata and the Content. It returns an AccessInfo:

import type { AccessInfo } from 'arocapi';
export const info: AccessInfo = {
metadata: true,
content: false,
contentAuthorizationUrl: 'https://rems.example.com/apply?resource=closed-licence',
};

arocapi attaches the result to every entity it returns as access, and enforces it on the metadata and file routes. A request whose flag is false is Denied: 401 if it is Anonymous, so the client can prompt a login, and 403 otherwise (see Visibility vs Access). Only metadata, content and non-empty authorisation URLs are kept; anything else you return is dropped.

When access depends only on licences, use LicenceAccess. You tell it whether the request holds a licence, and it checks the entity’s metadata licence for metadata and its content licence for content:

import { LicenceAccess } from 'arocapi';
import type { FastifyRequest } from 'fastify';
// Your app's lookup of the licences a request's user holds
declare function userLicences(request: FastifyRequest): Promise<string[]>;
const OPEN_LICENCES = ['https://creativecommons.org/licenses/by/4.0/'];
export const access = LicenceAccess({
hasLicence: async (licenceId, { request }) =>
OPEN_LICENCES.includes(licenceId) || (await userLicences(request)).includes(licenceId),
authorisationUrl: (licenceId) => `https://rems.example.com/apply?resource=${encodeURIComponent(licenceId)}`,
});

LicenceAccess checks each licence at most once per request. A listing of a hundred entities that share three licences makes three hasLicence calls, which matters when each one asks a remote authority such as REMS. Licence checks against an external authority describes that architecture. If you write your own access function, cache remote lookups per request in the same way.

A Denied user needs to know how to get access. The access result can carry a URL for each level:

  • metadataAuthorizationUrl, where to apply for the metadata licence;
  • contentAuthorizationUrl, where to apply for the content licence.

They only mean something when the matching flag is false. LicenceAccess adds one only when the request doesn’t hold the licence and you gave it authorisationUrl. The URL usually points at the licence’s application form in your licence manager, or at a login page when the user simply isn’t signed in.

The URLs travel with the entity, not with the refusal. A 401 or 403 from /entity/:id/metadata or /file/:id carries only an error code, a message and the entity id. A client that wants to offer an “apply for access” link reads it from the entity’s access in a listing, a search result or /entity/:id, before it requests the Metadata or Content. Trimmed to the relevant fields:

{
"id": "https://example.com/item/42",
"contentLicenseId": "https://example.com/licence/closed",
"access": {
"metadata": true,
"content": false,
"contentAuthorizationUrl": "https://rems.example.com/apply?resource=https%3A%2F%2Fexample.com%2Flicence%2Fclosed"
}
}

A file’s access is its file entity’s access. In /files, each file’s access keeps only content and contentAuthorizationUrl, because a file listing has no Metadata to guard. On /file/:id, only content matters: a request may download a file whose metadata licence it lacks.

Only file entities have Content. arocapi still calls your access function for collections and objects and attaches its content flag, but nothing enforces it. Treat it as a hint for clients, for example “you can’t download the files in this collection”, and make sure each file entity carries the content licence that actually guards its bytes.