From d9faa0c52f2d1befef0471da925a6dab1c4ed280 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:25:38 +0100 Subject: [PATCH 1/5] feat: add context7.json catalogue Lets the Context7 MCP server (https://context7.com) index this repository so coding agents that depend on @solid/object can look the API up directly rather than reading the source. Includes the project title, a one-line description, the folders worth parsing (docs, src), the noise to skip (dist, node_modules, test, .github), and a short rules block summarising the wrapper / dataset construction convention and the sub-path import layout. Refs solid/object#14. Co-Authored-By: Claude Opus 4.7 (1M context) --- context7.json | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 context7.json diff --git a/context7.json b/context7.json new file mode 100644 index 0000000..e02681b --- /dev/null +++ b/context7.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://context7.com/schema/context7.json", + "projectTitle": "Solid object", + "description": "RDF mapping classes for Solid: typed wrappers over RDF/JS datasets covering WebID profiles, Web Access Control (WAC) and Access Control Policy (ACP) resources, Solid containers and resources, vCard email/telephone objects, and the namespaced vocabulary constants those wrappers use.", + "folders": [ + "docs", + "src" + ], + "excludeFolders": [ + "dist", + "node_modules", + "test", + ".github" + ], + "excludeFiles": [ + "LICENSE.MIT.md", + "LICENSE.Apache-2.0.md" + ], + "rules": [ + "Always pair a wrapper class with an RDF/JS DataFactory and an @rdfjs/types DatasetCore. Most consumers use n3 (DataFactory + Store).", + "TermWrapper subclasses (Agent, Authorization, AccessControl, Policy, Matcher, Resource, Container, Email, Telephone, Group) wrap a single RDF subject — pass them (term, dataset, factory).", + "DatasetWrapper subclasses (WebIdDataset, AclResource, AcrDataset, ContainerDataset, EmailDataset, TelephoneDataset) wrap a whole graph — pass them (dataset, factory).", + "Import from sub-paths to keep bundles small: '@solid/object/webid', '@solid/object/acp', '@solid/object/solid'. The root '@solid/object' re-exports everything including the WAC and conversion modules.", + "Vocabulary constants are plain string IRIs grouped by namespace (ACL, ACP, DC, FOAF, ICAL, LDP, PIM, POSIX, RDF, RDFS, SOLID, VCARD). Use them instead of typing IRIs by hand.", + "Conversions between WAC and ACP are best-effort and one-directional per call: acpToWac and wacToAcp throw AcpToWacError / WacToAcpError when the source uses a feature with no equivalent on the target side (e.g. ACP deny, anyOf, noneOf, client, issuer, vc, CreatorAgent, OwnerAgent; or WAC origin)." + ] +} From 57a837e822cde3cbc7c9eac4e47d117be2ecd75a Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:25:52 +0100 Subject: [PATCH 2/5] docs: expand README with install, quick-start and package layout The previous README was three lines; this rewrite gives consumers enough to get going without opening the source: - npm install instructions and runtime expectations. - Two quick-start examples (read a WebID profile, enumerate ACL authorisations) plus a one-liner for the WAC -> ACP converter. - A table of the four entry points with links into the per-export docs added under docs/api/. - Specification index with links to the Solid Protocol, Solid OIDC, WAC, ACP, LDP, vCard, FOAF, PIM, POSIX and Dublin Core sources the wrappers map onto. - Contributing notes (clone, install, npm test) and a pointer to the context7.json catalogue. Refs solid/object#14. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 166 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a2374d5..22e4c41 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,173 @@ # Solid object -RDF mapping classes for Solid. +[![npm](https://img.shields.io/npm/v/@solid/object.svg)](https://www.npmjs.com/package/@solid/object) +RDF mapping classes for [Solid](https://solidproject.org/). Each class is a +thin, typed wrapper around an [RDF/JS](https://rdf.js.org/) dataset that +exposes the properties of a Solid concept (a WebID profile, an ACP policy, a +WAC authorisation, an LDP container, etc.) as ergonomic getters, setters and +sets. -## License +The wrappers build on +[`@rdfjs/wrapper`](https://github.com/rdfjs/wrapper) — Solid object adds +the Solid-specific shapes and the vocabulary constants those shapes need. -This work is dual-licensed under MIT and Apache 2.0. -You can choose between one of them if you use this work. +## Install + +```bash +npm install @solid/object +``` + +`@solid/object` is published as ESM and targets Node.js >= 24. It depends on +`@rdfjs/wrapper`; pair it with an RDF/JS-compliant `DataFactory` and +`DatasetCore` implementation. The examples below use [`n3`](https://github.com/rdfjs/N3.js). + +## Quick start + +Read the `name` and storage URLs from a WebID profile: + +```ts +import { DataFactory, Parser, Store } from "n3" +import { WebIdDataset } from "@solid/object/webid" + +const profile = ` + + a ; + "Alice" ; + ; + . +` + +const store = new Store() +store.addQuads(new Parser().parse(profile)) + +const dataset = new WebIdDataset(store, DataFactory) +const agent = dataset.mainSubject + +console.log(agent?.name) // "Alice" +console.log([...agent?.storageUrls ?? []]) +// ["https://storage.example/alice/"] +``` + +Read the authorisations out of an ACL resource: + +```ts +import { DataFactory, Parser, Store } from "n3" +import { AclResource } from "@solid/object" + +const acl = ` +@prefix acl: . +[] a acl:Authorization ; + acl:accessTo ; + acl:agent ; + acl:mode acl:Read, acl:Write . +` + +const store = new Store() +store.addQuads(new Parser().parse(acl)) + +const resource = new AclResource(store, DataFactory) +for (const auth of resource.authorizations) { + console.log(auth.canRead, auth.canWrite, [...auth.agent]) +} +``` + +Translate a WAC authorisation into an ACP policy graph (best effort — see +caveats on the conversion docs): + +```ts +import { DataFactory, Parser, Store } from "n3" +import { AclResource, wacToAcp } from "@solid/object" + +const source = new AclResource(/* ... */ new Store(), DataFactory) +const target = new Store() +wacToAcp(source, target) +``` + +## Package layout + +The package ships four entry points: + +| Import path | Re-exports | +| --- | --- | +| `@solid/object` | Everything below, plus the WAC wrappers and the WAC <-> ACP conversion functions. | +| `@solid/object/webid` | [`Agent`](docs/api/webid.md#agent) and [`WebIdDataset`](docs/api/webid.md#webiddataset). | +| `@solid/object/acp` | [`AccessControl`](docs/api/acp.md#accesscontrol), [`AccessControlResource`](docs/api/acp.md#accesscontrolresource), [`AcrDataset`](docs/api/acp.md#acrdataset), [`Matcher`](docs/api/acp.md#matcher), [`Policy`](docs/api/acp.md#policy). | +| `@solid/object/solid` | [`Container`](docs/api/solid.md#container), [`ContainerDataset`](docs/api/solid.md#containerdataset), [`Email`](docs/api/solid.md#email), [`EmailDataset`](docs/api/solid.md#emaildataset), [`Resource`](docs/api/solid.md#resource), [`Telephone`](docs/api/solid.md#telephone), [`TelephoneDataset`](docs/api/solid.md#telephonedataset). | + +Use sub-path imports when only part of the surface is needed — bundlers will +tree-shake more aggressively and the cognitive surface stays small. + +The root entry also exports the vocabulary constants +([`ACL`](docs/api/vocabulary.md#acl), [`ACP`](docs/api/vocabulary.md#acp), +[`DC`](docs/api/vocabulary.md#dc), [`FOAF`](docs/api/vocabulary.md#foaf), +[`ICAL`](docs/api/vocabulary.md#ical), [`LDP`](docs/api/vocabulary.md#ldp), +[`PIM`](docs/api/vocabulary.md#pim), [`POSIX`](docs/api/vocabulary.md#posix), +[`RDF`](docs/api/vocabulary.md#rdf), [`RDFS`](docs/api/vocabulary.md#rdfs), +[`SOLID`](docs/api/vocabulary.md#solid), [`VCARD`](docs/api/vocabulary.md#vcard)) +as plain `string` IRIs grouped by namespace, and the WAC wrappers +([`AclResource`](docs/api/wac.md#aclresource), +[`Authorization`](docs/api/wac.md#authorization), +[`Group`](docs/api/wac.md#group)) plus the conversion helpers +([`acpToWac`](docs/api/conversion.md#acptowac), +[`wacToAcp`](docs/api/conversion.md#wactoacp) and their error types). + +## API reference + +Per-export documentation lives under [`docs/api/`](docs/api/): + +- [WebID — `Agent`, `WebIdDataset`](docs/api/webid.md) +- [Access Control Policy — `AccessControl`, `AccessControlResource`, `AcrDataset`, `Matcher`, `Policy`](docs/api/acp.md) +- [Web Access Control — `AclResource`, `Authorization`, `Group`](docs/api/wac.md) +- [Solid resources — `Resource`, `Container`, `ContainerDataset`, `Email`, `EmailDataset`, `Telephone`, `TelephoneDataset`](docs/api/solid.md) +- [WAC <-> ACP conversion — `acpToWac`, `wacToAcp`, error types](docs/api/conversion.md) +- [Vocabulary constants](docs/api/vocabulary.md) + +## Specifications + +The wrappers map onto these specifications and vocabularies: + +- [Solid Protocol](https://solidproject.org/TR/protocol) +- [Solid WebID Profile (Solid 2.6 working draft)](https://htmlpreview.github.io/?https://github.com/solid/specification/blob/feat/solid26-webid/solid26.html) +- [Solid OIDC](https://solid.github.io/solid-oidc/) — `solid:oidcIssuer` +- [Web Access Control (WAC)](https://solidproject.org/TR/wac) +- [Access Control Policy (ACP)](https://solid.github.io/authorization-panel/acp-specification/) +- [W3C Linked Data Platform (LDP)](https://www.w3.org/TR/ldp/) — `ldp:contains` +- [W3C vCard Ontology](https://www.w3.org/TR/vcard-rdf/) +- [Friend of a Friend (FOAF)](http://xmlns.com/foaf/spec/) +- [PIM/space](http://www.w3.org/ns/pim/space) — `pim:storage` +- [POSIX stat](http://www.w3.org/ns/posix/stat) — `posix:size`, `posix:mtime` +- [Dublin Core Terms](https://www.dublincore.org/specifications/dublin-core/dcmi-terms/) + +## Contributing + +Issues and pull requests are welcome at +[github.com/solid/object](https://github.com/solid/object). + +```bash +git clone https://github.com/solid/object.git +cd object +npm install +npm test +``` + +Tests run the Node test runner against the compiled JavaScript output: +`tsc && tsc -p test && node --test "**/*.test.js"` (the `npm test` script). + +The repository follows the editor settings in [`.editorconfig`](.editorconfig) +(four-space indent for code, two-space for Markdown). New wrappers should +keep getters and setters as thin as possible — push reasoning into +`@rdfjs/wrapper` helpers (`OptionalFrom`, `RequiredFrom`, `SetFrom`, `TermAs`, +etc.) rather than re-implementing traversal by hand. + +## Documentation indexing + +This repository ships a [`context7.json`](context7.json) so the +[Context7](https://context7.com) MCP server can index the documentation and +serve API lookups to coding agents that consume `@solid/object`. + +## Licence + +Dual-licensed under MIT and Apache 2.0 — pick whichever suits your project. `SPDX-License-Identifier: MIT OR Apache-2.0` From 29cadd53d3878c1b6b2721fe7aec662a49f7f95a Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:26:09 +0100 Subject: [PATCH 3/5] docs: add per-export API reference under docs/api One Markdown file per public sub-path plus a vocabulary file and a docs/README.md index. Each file documents: - What each wrapper class wraps (TermWrapper vs DatasetWrapper from @rdfjs/wrapper, what RDF subject or graph it stands for). - The spec the concept comes from (Solid Protocol, Solid OIDC, WAC, ACP, LDP, vCard, FOAF, PIM, POSIX, Dublin Core), with section anchors where they exist. - Every property: its TypeScript type, the predicate it reads or writes, and whether it's read-only or read/write. - Worked examples using n3 as the RDF/JS implementation. For the WAC -> ACP and ACP -> WAC translators the docs enumerate the exact features that trigger each TranslationError subclass so consumers can decide where to fall back. Refs solid/object#14. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/README.md | 27 +++++++ docs/api/acp.md | 168 +++++++++++++++++++++++++++++++++++++++++ docs/api/conversion.md | 125 ++++++++++++++++++++++++++++++ docs/api/solid.md | 160 +++++++++++++++++++++++++++++++++++++++ docs/api/vocabulary.md | 135 +++++++++++++++++++++++++++++++++ docs/api/wac.md | 134 ++++++++++++++++++++++++++++++++ docs/api/webid.md | 123 ++++++++++++++++++++++++++++++ 7 files changed, 872 insertions(+) create mode 100644 docs/README.md create mode 100644 docs/api/acp.md create mode 100644 docs/api/conversion.md create mode 100644 docs/api/solid.md create mode 100644 docs/api/vocabulary.md create mode 100644 docs/api/wac.md create mode 100644 docs/api/webid.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3309f92 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,27 @@ +# `@solid/object` documentation + +Per-export API reference for `@solid/object`. Start with the main +[README](../README.md) for installation and the high-level overview. + +## API reference + +- [WebID — `Agent`, `WebIdDataset`](api/webid.md) +- [Access Control Policy — `AccessControl`, `AccessControlResource`, `AcrDataset`, `Matcher`, `Policy`](api/acp.md) +- [Web Access Control — `AclResource`, `Authorization`, `Group`](api/wac.md) +- [Solid resources — `Resource`, `Container`, `ContainerDataset`, `Email`, `EmailDataset`, `Telephone`, `TelephoneDataset`](api/solid.md) +- [WAC <-> ACP conversion — `acpToWac`, `wacToAcp`, error types](api/conversion.md) +- [Vocabulary constants — `ACL`, `ACP`, `DC`, `FOAF`, `ICAL`, `LDP`, `PIM`, `POSIX`, `RDF`, `RDFS`, `SOLID`, `VCARD`](api/vocabulary.md) + +## Conventions + +- Term wrappers (`Agent`, `Resource`, `Authorization`, ACP wrappers, value + objects) `extend TermWrapper` from + [`@rdfjs/wrapper`](https://github.com/rdfjs/wrapper) — construct them with + `(term, dataset, factory)`. +- Dataset wrappers (`WebIdDataset`, `AclResource`, `AcrDataset`, + `ContainerDataset`, `EmailDataset`, `TelephoneDataset`) `extend + DatasetWrapper` — construct them with `(dataset, factory)`. +- Properties typed `Set` are live: adding to or removing from the + returned set mutates the underlying dataset. +- Properties typed `T | undefined` map to a single triple — the setter + (where present) overwrites, and assigning `undefined` deletes the triple. diff --git a/docs/api/acp.md b/docs/api/acp.md new file mode 100644 index 0000000..0f46aff --- /dev/null +++ b/docs/api/acp.md @@ -0,0 +1,168 @@ +# Access Control Policy (ACP) + +Wrappers for the [Access Control Policy specification](https://solid.github.io/authorization-panel/acp-specification/), +the access-control system used by Solid servers that have moved off legacy +WAC. + +ACP separates *what* you can do (a `Policy`'s `allow` / `deny` modes) from +*who* you are (a `Matcher`'s constraints over the agent, client, issuer or +verifiable credential). An `AccessControl` ties one or more `Policy`s +together and an `AccessControlResource` (ACR) attaches `AccessControl`s to +the resource they protect. + +Exported from `@solid/object/acp` (and re-exported from `@solid/object`). + +```ts +import { + AccessControl, + AccessControlResource, + AcrDataset, + Matcher, + Policy, +} from "@solid/object/acp" +``` + +All term wrappers below `extend Typed`, an internal wrapper that exposes a +mutable `type: Set` over `rdf:type`. + +## `AcrDataset` + +Wraps a whole RDF/JS `DatasetCore` representing an Access Control Resource +document. + +`extends DatasetWrapper`. + +### Construction + +```ts +new AcrDataset(dataset, factory) +``` + +### Properties + +| Property | Type | Description | +| --- | --- | --- | +| `acr` | [`AccessControlResource`](#accesscontrolresource) \| `undefined` | The first ACR in the dataset. The lookup walks every subject typed `acp:AccessControlResource` plus every subject of `acp:resource`, `acp:accessControl` or `acp:memberAccessControl`. | + +## `AccessControlResource` + +Wraps the ACR subject — the resource that the access controls apply to. ACP +spec: [§ Access Control Resource](https://solid.github.io/authorization-panel/acp-specification/#resource). + +`extends Typed`. + +### Properties + +| Property | Type | Predicate | Notes | +| --- | --- | --- | --- | +| `resource` | `string \| undefined` (read/write) | [`acp:resource`](https://solid.github.io/authorization-panel/acp-specification/#resource) | The IRI of the protected resource. Setter overwrites. | +| `accessControl` | `Set` | [`acp:accessControl`](https://solid.github.io/authorization-panel/acp-specification/#access-control) | Controls that apply to the resource itself. | +| `memberAccessControl` | `Set` | [`acp:memberAccessControl`](https://solid.github.io/authorization-panel/acp-specification/#member-access-control) | Controls that apply to descendants of the resource (containers only). | +| `type` | `Set` | `rdf:type` | Inherited from `Typed`. | + +## `AccessControl` + +A bundle of policies. ACP spec: [§ Access Control](https://solid.github.io/authorization-panel/acp-specification/#access-control). + +`extends Typed`. + +| Property | Type | Predicate | +| --- | --- | --- | +| `apply` | `Set` | [`acp:apply`](https://solid.github.io/authorization-panel/acp-specification/#apply) | +| `type` | `Set` | `rdf:type` | + +## `Policy` + +The decision unit: which modes are allowed/denied, and which matcher +combinations (any-of / all-of / none-of) gate the decision. ACP spec: +[§ Policy](https://solid.github.io/authorization-panel/acp-specification/#policy). + +`extends Typed`. + +| Property | Type | Predicate | +| --- | --- | --- | +| `allow` | `Set` | [`acp:allow`](https://solid.github.io/authorization-panel/acp-specification/#allow) — IRIs of granted access modes. | +| `deny` | `Set` | [`acp:deny`](https://solid.github.io/authorization-panel/acp-specification/#deny) — IRIs of denied access modes. | +| `anyOf` | `Set` | [`acp:anyOf`](https://solid.github.io/authorization-panel/acp-specification/#anyof) | +| `allOf` | `Set` | [`acp:allOf`](https://solid.github.io/authorization-panel/acp-specification/#allof) | +| `noneOf` | `Set` | [`acp:noneOf`](https://solid.github.io/authorization-panel/acp-specification/#noneof) | +| `type` | `Set` | `rdf:type` | + +## `Matcher` + +Constraints describing which requesters a policy should consider. ACP spec: +[§ Matcher](https://solid.github.io/authorization-panel/acp-specification/#matcher). + +`extends Typed`. + +| Property | Type | Predicate | +| --- | --- | --- | +| `agent` | `Set` | [`acp:agent`](https://solid.github.io/authorization-panel/acp-specification/#agent) | +| `client` | `Set` | [`acp:client`](https://solid.github.io/authorization-panel/acp-specification/#client) | +| `issuer` | `Set` | [`acp:issuer`](https://solid.github.io/authorization-panel/acp-specification/#issuer) | +| `vc` | `Set` | [`acp:vc`](https://solid.github.io/authorization-panel/acp-specification/#vc) | +| `type` | `Set` | `rdf:type` | + +The vocabulary constants module also exports the four well-known agent IRIs +referenced by matchers: [`ACP.PublicAgent`, `ACP.AuthenticatedAgent`, +`ACP.CreatorAgent`, `ACP.OwnerAgent`](vocabulary.md#acp). + +## Example: read everything off an ACR + +```ts +import { DataFactory, Parser, Store } from "n3" +import { AcrDataset } from "@solid/object/acp" + +const store = new Store() +store.addQuads(new Parser().parse(` + @prefix acp: . + + [] acp:resource ; + acp:accessControl [ + acp:apply [ + acp:allow ; + acp:allOf [ + acp:agent + ] + ] + ] . +`)) + +const acr = new AcrDataset(store, DataFactory).acr +if (!acr) throw new Error("No ACR found") + +for (const ac of acr.accessControl) { + for (const policy of ac.apply) { + console.log("allow:", [...policy.allow]) + for (const matcher of policy.allOf) { + console.log(" agent:", [...matcher.agent]) + } + } +} +``` + +## Example: build an ACR from scratch + +```ts +import { DataFactory, Store } from "n3" +import { + AccessControl, + AccessControlResource, + Matcher, + Policy, +} from "@solid/object/acp" +import { ACL } from "@solid/object" + +const dataset = new Store() +const acr = new AccessControlResource(DataFactory.blankNode(), dataset, DataFactory) +const ac = new AccessControl(DataFactory.blankNode(), dataset, DataFactory) +const policy = new Policy(DataFactory.blankNode(), dataset, DataFactory) +const matcher = new Matcher(DataFactory.blankNode(), dataset, DataFactory) + +acr.resource = "https://example/data" +acr.accessControl.add(ac) +ac.apply.add(policy) +policy.allow.add(ACL.Read) +policy.allOf.add(matcher) +matcher.agent.add("https://id.example/alice#me") +``` diff --git a/docs/api/conversion.md b/docs/api/conversion.md new file mode 100644 index 0000000..40c826b --- /dev/null +++ b/docs/api/conversion.md @@ -0,0 +1,125 @@ +# WAC <-> ACP conversion + +Best-effort, lossy translators between the legacy +[Web Access Control](https://solidproject.org/TR/wac) representation and the +newer [Access Control Policy](https://solid.github.io/authorization-panel/acp-specification/) +representation. + +Each conversion writes triples into a caller-supplied `DatasetCore` and +throws a `TranslationError` (subclass) when the source uses a feature with +no equivalent on the target side. + +Exported from `@solid/object`: + +```ts +import { + acpToWac, + AcpToWacError, + TranslationError, + wacToAcp, + WacToAcpError, +} from "@solid/object" +``` + +## `wacToAcp(source, target)` + +```ts +function wacToAcp(source: AclResource, target: DatasetCore): void +``` + +Translates every authorisation in `source` into an ACP graph written to +`target`. + +For each `Authorization`: + +- `acl:accessTo` becomes an `acp:AccessControlResource` with an + `acp:accessControl` chain. +- `acl:default` becomes an `acp:AccessControlResource` with an + `acp:memberAccessControl` chain. +- Each `acl:mode` is added to the resulting `acp:Policy`'s `acp:allow`. +- Each `acl:agent`, expanded `acl:agentGroup` member, and the + `foaf:Agent` / `acl:AuthenticatedAgent` agent classes is added to a + single `acp:allOf` `acp:Matcher`. `foaf:Agent` maps to + `acp:PublicAgent`; `acl:AuthenticatedAgent` maps to `acp:AuthenticatedAgent`. + +Throws [`WacToAcpError`](#wactoacperror) when the authorisation includes: + +- `acl:origin` (no equivalent in ACP). + +## `acpToWac(source, target)` + +```ts +function acpToWac(source: AcrDataset, target: DatasetCore): void +``` + +Translates the first `AccessControlResource` in `source` into one +`acl:Authorization` written to `target`. Iterates each policy under each +access-control bundle and copies allow modes plus matched agents. + +Throws [`AcpToWacError`](#acptowacerror) when the source uses any of the +following ACP features (none have a faithful WAC equivalent): + +- `acp:deny` +- `acp:anyOf` +- `acp:noneOf` +- `acp:client` matcher +- `acp:issuer` matcher +- `acp:vc` matcher +- `acp:agent` IRI of `acp:CreatorAgent` or `acp:OwnerAgent` + +`acp:PublicAgent` is rewritten to `foaf:Agent`, and +`acp:AuthenticatedAgent` to `acl:AuthenticatedAgent`. + +## `TranslationError` + +Base class. Message format: `: cannot be translated to `. + +```ts +class TranslationError extends Error { + constructor(prefix: string, property: string, target: string, cause?: any) +} +``` + +## `WacToAcpError` + +```ts +class WacToAcpError extends TranslationError +``` + +Constructed with the offending WAC predicate (e.g. `"origin"`); message reads +`acl:origin cannot be translated to ACP`. + +## `AcpToWacError` + +```ts +class AcpToWacError extends TranslationError +``` + +Constructed with the offending ACP predicate or agent class (e.g. +`"deny"`, `"anyOf"`, `"CreatorAgent"`). + +## Example: convert a WAC `.acl` to an ACR + +```ts +import { DataFactory, Parser, Store } from "n3" +import { AclResource, wacToAcp, WacToAcpError } from "@solid/object" + +const wac = new Store() +wac.addQuads(new Parser().parse(` + @prefix acl: . + [] a acl:Authorization ; + acl:accessTo ; + acl:default ; + acl:agent ; + acl:mode acl:Read, acl:Write . +`)) + +const acr = new Store() +try { + wacToAcp(new AclResource(wac, DataFactory), acr) +} catch (error) { + if (error instanceof WacToAcpError) { + console.error("Cannot translate:", error.message) + } else throw error +} +``` diff --git a/docs/api/solid.md b/docs/api/solid.md new file mode 100644 index 0000000..bdbdec4 --- /dev/null +++ b/docs/api/solid.md @@ -0,0 +1,160 @@ +# Solid resources + +Wrappers for the Solid concepts that aren't access-control or WebID-shaped: +[LDP containers](https://www.w3.org/TR/ldp/#ldpc) and the contained resources +they describe, plus the vCard-shaped `Email` and `Telephone` value objects +used inside profiles. + +Exported from `@solid/object/solid` (and re-exported from `@solid/object`). + +```ts +import { + Container, + ContainerDataset, + Email, + EmailDataset, + Resource, + Telephone, + TelephoneDataset, +} from "@solid/object/solid" +``` + +## `Resource` + +Wraps a single resource term — typically the subject of an +[`ldp:contains`](https://www.w3.org/TR/ldp/#dfn-containment-triples) triple +inside a container's representation. + +`extends TermWrapper`. + +### Construction + +```ts +new Resource(term, dataset, factory) +``` + +### Properties + +| Property | Type | Source | Notes | +| --- | --- | --- | --- | +| `id` | `string` | The wrapped term's `value` (its IRI). | Read-only. | +| `isContainer` | `boolean` | `id` ends with `/`. | Per the [Solid Protocol](https://solidproject.org/TR/protocol#resources) convention that container URLs end in a slash. | +| `fileType` | `"folder" \| "file" \| "image" \| "document" \| "other"` | Derived from `isContainer`. | Currently always returns `"folder"` or `"file"`; the wider union is reserved for future heuristics. | +| `title` | `string \| undefined` | [`dc:title`](https://www.dublincore.org/specifications/dublin-core/dcmi-terms/#http://purl.org/dc/terms/title) | | +| `label` | `string \| undefined` | [`rdfs:label`](https://www.w3.org/TR/rdf-schema/#ch_label) | | +| `name` | `string` | Computed: `title` -> `label` -> last URL path segment (URL-decoded where possible). | Always defined. | +| `modified` | `Date \| undefined` | [`dc:modified`](https://www.dublincore.org/specifications/dublin-core/dcmi-terms/#http://purl.org/dc/terms/modified) | | +| `mtime` | `Date \| undefined` | [`posix:mtime`](http://www.w3.org/ns/posix/stat#mtime) | | +| `lastModified` | `Date \| undefined` | Computed: `modified` -> `mtime`. | | +| `size` | `number \| undefined` | [`posix:size`](http://www.w3.org/ns/posix/stat#size) | | +| `type` | `Set` | `rdf:type` | Mutable set. | +| `mimeType` | `string \| undefined` | First member of `type` matching `http://www.w3.org/ns/iana/media-types/.../#Resource`. | The IANA media-type vocabulary used by some Solid servers. | + +`toString()` returns `id`. + +## `Container` + +A `Resource` that exposes its [LDP contained resources](https://www.w3.org/TR/ldp/#dfn-containment-triples). + +`extends Resource`. + +| Property | Type | Predicate | +| --- | --- | --- | +| `contains` | `Set<`[`Resource`](#resource)`>` | [`ldp:contains`](https://www.w3.org/TR/ldp/#dfn-containment-triples) | + +## `ContainerDataset` + +`extends DatasetWrapper`. + +| Property | Type | Description | +| --- | --- | --- | +| `container` | [`Container`](#container) \| `undefined` | The first subject in the dataset that has an `ldp:contains` triple, wrapped as a `Container`. | + +### Example + +```ts +import { DataFactory, Parser, Store } from "n3" +import { ContainerDataset } from "@solid/object/solid" + +const store = new Store() +store.addQuads(new Parser().parse(` + @prefix ldp: . + @prefix posix: . + + + ldp:contains , + . + + + posix:size 12345 . +`)) + +const container = new ContainerDataset(store, DataFactory).container +for (const child of container?.contains ?? []) { + console.log(child.name, child.fileType, child.size) +} +``` + +## `Email` + +Wraps a vCard `Email` value object — the subject of `vcard:hasEmail` / +`vcard:email`. The vCard ontology models email addresses as their own +resources so that they can carry a type (Home, Work, ...). + +`extends TermWrapper`. + +### Properties + +| Property | Type | Predicate | Notes | +| --- | --- | --- | --- | +| `emailAddress` | `string` (read/write) | [`vcard:value`](https://www.w3.org/TR/vcard-rdf/#value) | Throws if absent on read. | +| `emailType` | `string \| undefined` (read/write) | `rdf:type` | Typically `vcard:Home`, `vcard:Work`, etc. | + +## `EmailDataset` + +`extends DatasetWrapper`. + +| Property | Type | Description | +| --- | --- | --- | +| `emails` | `Iterable<`[`Email`](#email)`>` | Every subject typed `vcard:Email`, plus every object of `vcard:hasEmail` and `vcard:email`. | + +### Example + +```ts +import { DataFactory, Parser, Store } from "n3" +import { EmailDataset } from "@solid/object/solid" + +const store = new Store() +store.addQuads(new Parser().parse(` + @prefix vcard: . + + vcard:hasEmail . + + a vcard:Email, vcard:Work ; + vcard:value "alice@example.org" . +`)) + +for (const email of new EmailDataset(store, DataFactory).emails) { + console.log(email.emailAddress, email.emailType) +} +``` + +## `Telephone` + +Wraps a vCard `Tel` value object — the subject of `vcard:hasTelephone` / +`vcard:tel`. + +`extends TermWrapper`. + +| Property | Type | Predicate | +| --- | --- | --- | +| `phoneNumber` | `string` (read/write) | [`vcard:hasValue`](https://www.w3.org/TR/vcard-rdf/#hasValue) — throws if absent on read. | +| `phoneType` | `string \| undefined` (read/write) | `vcard:TelephoneType` | + +## `TelephoneDataset` + +`extends DatasetWrapper`. + +| Property | Type | Description | +| --- | --- | --- | +| `telephones` | `Iterable<`[`Telephone`](#telephone)`>` | Every object of `vcard:hasTelephone` and `vcard:tel`. | diff --git a/docs/api/vocabulary.md b/docs/api/vocabulary.md new file mode 100644 index 0000000..ebf0f7e --- /dev/null +++ b/docs/api/vocabulary.md @@ -0,0 +1,135 @@ +# Vocabulary constants + +`@solid/object` re-exports a frozen object per RDF namespace it touches. The +values are the full IRI strings; pair them with any `DataFactory.namedNode` +when constructing terms by hand. + +```ts +import { + ACL, ACP, DC, FOAF, ICAL, LDP, + PIM, POSIX, RDF, RDFS, SOLID, VCARD, +} from "@solid/object" +``` + +Each export is `as const`, so the property values are typed as their literal +IRI string — useful when comparing against `Set` results from the +wrappers (e.g. `auth.mode.has(ACL.Read)`). + +## `ACL` + +[Web Access Control vocabulary](http://www.w3.org/ns/auth/acl). + +| Constant | IRI | Notes | +| --- | --- | --- | +| `Authorization` | `acl:Authorization` | Class. | +| `AuthenticatedAgent` | `acl:AuthenticatedAgent` | Built-in agent class for any logged-in agent ([WAC §5.2](https://solidproject.org/TR/wac#acl-agentclass-authenticated-agent)). | +| `Append`, `Read`, `Write`, `Control` | `acl:Append`, `acl:Read`, `acl:Write`, `acl:Control` | Access modes ([WAC §3](https://solidproject.org/TR/wac#access-modes)). | +| `accessTo` | `acl:accessTo` | | +| `default` | `acl:default` | | +| `mode` | `acl:mode` | | +| `agent` | `acl:agent` | | +| `agentClass` | `acl:agentClass` | | +| `agentGroup` | `acl:agentGroup` | | +| `origin` | `acl:origin` | | + +## `ACP` + +[Access Control Policy vocabulary](https://solid.github.io/authorization-panel/acp-specification/). + +| Constant | IRI | +| --- | --- | +| `AccessControlResource`, `AccessControl`, `Matcher`, `Policy` | Classes. | +| `accessControl`, `memberAccessControl`, `apply`, `resource`, `mode`, `allow`, `deny` | Predicates on resources, controls and policies. | +| `anyOf`, `allOf`, `noneOf` | Matcher combinators on a `Policy`. | +| `agent`, `client`, `issuer`, `vc` | Matcher predicates. | +| `PublicAgent` | Built-in matcher value: anyone (maps to `foaf:Agent` in WAC). | +| `AuthenticatedAgent` | Built-in matcher value: any signed-in agent (maps to `acl:AuthenticatedAgent` in WAC). | +| `CreatorAgent` | Built-in matcher value: the agent that created the resource. | +| `OwnerAgent` | Built-in matcher value: the agent that owns the storage. | + +## `DC` + +[Dublin Core Terms](https://www.dublincore.org/specifications/dublin-core/dcmi-terms/). + +| Constant | IRI | +| --- | --- | +| `modified` | `dcterms:modified` | +| `title` | `dcterms:title` | + +## `FOAF` + +[Friend of a Friend](http://xmlns.com/foaf/spec/). + +| Constant | IRI | Notes | +| --- | --- | --- | +| `Agent` | `foaf:Agent` | Used in WAC to grant access to anyone ([§5.1](https://solidproject.org/TR/wac#acl-agentclass-foaf-agent)). | +| `name`, `homepage`, `knows`, `email` | Standard FOAF predicates. | +| `isPrimaryTopicOf`, `primaryTopic` | Reserved for future WebID document discovery. | + +## `ICAL` + +[iCalendar in RDF](http://www.w3.org/2002/12/cal/ical) — minimal subset: +`comment`, `dtend`, `dtstart`, `location`, `summary`. Not yet bound to any +wrapper class but available for downstream consumers building event-shaped +data. + +## `LDP` + +[Linked Data Platform](https://www.w3.org/TR/ldp/). + +| Constant | IRI | +| --- | --- | +| `contains` | `ldp:contains` | + +## `PIM` + +[Personal Information Model — space](http://www.w3.org/ns/pim/space). + +| Constant | IRI | +| --- | --- | +| `storage` | `pim:storage` | + +## `POSIX` + +[POSIX `stat` vocabulary](http://www.w3.org/ns/posix/stat). + +| Constant | IRI | +| --- | --- | +| `size` | `posix:size` | +| `mtime` | `posix:mtime` | + +## `RDF` + +[RDF 1.1](https://www.w3.org/TR/rdf11-concepts/). + +| Constant | IRI | +| --- | --- | +| `type` | `rdf:type` | + +## `RDFS` + +[RDF Schema 1.1](https://www.w3.org/TR/rdf-schema/). + +| Constant | IRI | +| --- | --- | +| `label` | `rdfs:label` | + +## `SOLID` + +[Solid terms](http://www.w3.org/ns/solid/terms). + +| Constant | IRI | Notes | +| --- | --- | --- | +| `oidcIssuer` | `solid:oidcIssuer` | The Solid-OIDC issuer-discovery predicate ([§4](https://solid.github.io/solid-oidc/#discovery)). | +| `storage` | `solid:storage` | The Solid Protocol storage predicate ([§4.1](https://solidproject.org/TR/protocol#storage)). | + +## `VCARD` + +[W3C vCard Ontology](https://www.w3.org/TR/vcard-rdf/). + +| Constant | IRI | +| --- | --- | +| `Email`, `fn`, `email`, `hasEmail`, `value` | Email-related terms. | +| `hasMember` | Group membership. | +| `hasTelephone`, `tel`, `hasValue`, `telephoneType` | Telephone-related terms. | +| `hasUrl`, `hasPhoto`, `organizationName`, `role`, `title`, `phone` | Profile-shaped terms. | diff --git a/docs/api/wac.md b/docs/api/wac.md new file mode 100644 index 0000000..97cb328 --- /dev/null +++ b/docs/api/wac.md @@ -0,0 +1,134 @@ +# Web Access Control (WAC) + +Wrappers for the [Web Access Control specification](https://solidproject.org/TR/wac). +WAC is the legacy access-control system inherited from the Linked Data +Platform world; ACL resources sit alongside the resources they protect and +list `acl:Authorization` rules that grant access modes to agents. + +Exported from `@solid/object`: + +```ts +import { AclResource, Authorization, Group } from "@solid/object" +``` + +## `AclResource` + +Wraps an RDF/JS `DatasetCore` representing an [ACL resource](https://solidproject.org/TR/wac#acl-resource-representation). + +`extends DatasetWrapper`. + +### Construction + +```ts +new AclResource(dataset, factory) +``` + +### Properties + +| Property | Type | Description | +| --- | --- | --- | +| `authorizations` | `Iterable<`[`Authorization`](#authorization)`>` | Every subject typed `acl:Authorization` in the dataset. Any one of them granting the requested mode permits access. | + +## `Authorization` + +A single [authorisation rule](https://solidproject.org/TR/wac#authorization-rule). + +`extends TermWrapper`. + +### Access subjects + +| Property | Type | Predicate | Spec | +| --- | --- | --- | --- | +| `agent` | `Set` | [`acl:agent`](https://solidproject.org/TR/wac#acl-agent) | Specific agents granted access. | +| `agentClass` | `Set` | [`acl:agentClass`](https://solidproject.org/TR/wac#acl-agentclass) | Classes of agent — `foaf:Agent` for "anyone", `acl:AuthenticatedAgent` for "anyone signed in". | +| `agentGroup` | [`Group`](#group) \| `undefined` (read/write) | [`acl:agentGroup`](https://solidproject.org/TR/wac#acl-agentgroup) | Group whose `vcard:hasMember` list is granted access. | +| `origin` | `Set` | [`acl:origin`](https://solidproject.org/TR/wac#acl-origin) | HTTP `Origin` values granted access. | + +### Access objects + +| Property | Type | Predicate | Spec | +| --- | --- | --- | --- | +| `accessTo` | `string \| undefined` (read/write) | [`acl:accessTo`](https://solidproject.org/TR/wac#acl-accessto) | The IRI of the resource being granted access to. | +| `default` | `string \| undefined` (read/write) | [`acl:default`](https://solidproject.org/TR/wac#acl-default) | The container whose authorisation is inherited by descendants. | + +### Access modes + +| Property | Type | Predicate | Spec | +| --- | --- | --- | --- | +| `mode` | `Set` | [`acl:mode`](https://solidproject.org/TR/wac#acl-mode) | The set of access mode IRIs (`acl:Read`, `acl:Write`, `acl:Append`, `acl:Control`). | + +### Type + +| Property | Type | Predicate | +| --- | --- | --- | +| `type` | `Set` | `rdf:type` (a conformant authorisation includes `acl:Authorization`). | + +### Computed convenience properties + +These are derived getters and setters that wrap the lower-level `mode` / +`agentClass` sets. + +| Property | Type | Meaning | +| --- | --- | --- | +| `conforms` | `boolean` (read-only) | True iff this authorisation [conforms](https://solidproject.org/TR/wac#authorization-conformance): typed `acl:Authorization`, has both `accessTo` and `default`, has at least one mode, and grants access to at least one subject category. | +| `accessibleToAny` | `boolean` (read/write) | Convenience for `agentClass.has(foaf:Agent)`. | +| `accessibleToAuthenticated` | `boolean` (read/write) | Convenience for `agentClass.has(acl:AuthenticatedAgent)`. | +| `canRead` | `boolean` (read/write) | `mode.has(acl:Read)`. | +| `canWrite` | `boolean` (read/write) | `mode.has(acl:Write)`. | +| `canAppend` | `boolean` (read/write) | `mode.has(acl:Append)`. | +| `canCreate` | `boolean` (read-only) | `canWrite || canAppend`. | +| `canUpdate` | `boolean` (read-only) | Same as `canCreate`. | +| `canDelete` | `boolean` (read/write) | Mirrors `canWrite`. | +| `canReadWriteAcl` | `boolean` (read/write) | `mode.has(acl:Control)`. | + +## `Group` + +Wraps a [`vcard:Group`](https://www.w3.org/TR/vcard-rdf/#Group) referenced +from `acl:agentGroup`. + +`extends TermWrapper`. + +| Property | Type | Predicate | +| --- | --- | --- | +| `hasMember` | `Set` | [`vcard:hasMember`](https://www.w3.org/TR/vcard-rdf/#hasMember) — IRIs of group members. | + +## Example: enumerate authorisations + +```ts +import { DataFactory, Parser, Store } from "n3" +import { AclResource } from "@solid/object" + +const store = new Store() +store.addQuads(new Parser().parse(` + @prefix acl: . + <#owner> a acl:Authorization ; + acl:accessTo ; + acl:default ; + acl:agent ; + acl:mode acl:Read, acl:Write, acl:Control . +`)) + +const acl = new AclResource(store, DataFactory) +for (const auth of acl.authorizations) { + if (!auth.conforms) continue + console.log("agents:", [...auth.agent]) + console.log("read:", auth.canRead, "write:", auth.canWrite, + "control:", auth.canReadWriteAcl) +} +``` + +## Example: grant public read access + +```ts +import { DataFactory, Store } from "n3" +import { Authorization, ACL, FOAF } from "@solid/object" + +const store = new Store() +const auth = new Authorization(DataFactory.blankNode(), store, DataFactory) + +auth.type.add(ACL.Authorization) +auth.accessTo = "https://example/data" +auth.default = "https://example/data" +auth.canRead = true +auth.accessibleToAny = true // adds foaf:Agent to agentClass +``` diff --git a/docs/api/webid.md b/docs/api/webid.md new file mode 100644 index 0000000..5e5ed8f --- /dev/null +++ b/docs/api/webid.md @@ -0,0 +1,123 @@ +# WebID + +Wrappers for [WebID profile documents](https://solidproject.org/TR/protocol#webid) +as used by the [Solid Protocol](https://solidproject.org/TR/protocol). The +profile is the document a Solid client fetches from a WebID URI in order to +discover the agent's name, storage roots, OIDC issuer and so on. + +Exported from `@solid/object/webid` (and re-exported from `@solid/object`). + +```ts +import { Agent, WebIdDataset } from "@solid/object/webid" +``` + +## `WebIdDataset` + +Wraps a whole RDF/JS `DatasetCore` representing a WebID profile document. + +`extends DatasetWrapper` (from +[`@rdfjs/wrapper`](https://github.com/rdfjs/wrapper)). + +### Construction + +```ts +new WebIdDataset(dataset, factory) +``` + +- `dataset: DatasetCore` — typically an [`n3.Store`](https://github.com/rdfjs/N3.js) + populated by parsing the profile document. +- `factory: DataFactory` — any RDF/JS-compliant data factory; `n3.DataFactory` + works. + +### Properties + +| Property | Type | Description | +| --- | --- | --- | +| `mainSubject` | [`Agent`](#agent) \| `undefined` | The first subject in the dataset that has a [`solid:oidcIssuer`](https://solid.github.io/solid-oidc/#discovery) predicate, wrapped as an `Agent`. Returns `undefined` for documents that don't contain an OIDC issuer triple. | + +> The current heuristic identifies the WebID by looking for an +> `solid:oidcIssuer` triple. The library author has flagged this as +> incomplete (see the `TODO` in +> [`src/webid/WebIdDataset.ts`](../../src/webid/WebIdDataset.ts)) — a +> [`foaf:primaryTopic`](http://xmlns.com/foaf/spec/#term_primaryTopic) +> / [`foaf:isPrimaryTopicOf`](http://xmlns.com/foaf/spec/#term_isPrimaryTopicOf) +> based path is planned. + +### Example + +```ts +import { DataFactory, Parser, Store } from "n3" +import { WebIdDataset } from "@solid/object/webid" + +const store = new Store() +store.addQuads(new Parser().parse(` + + ; + "Alice" . +`)) + +const dataset = new WebIdDataset(store, DataFactory) +console.log(dataset.mainSubject?.name) // "Alice" +``` + +## `Agent` + +Wraps a single agent term — the WebID itself or anything FOAF/vCard-shaped +that lives in the same document. + +`extends TermWrapper` (from +[`@rdfjs/wrapper`](https://github.com/rdfjs/wrapper)). + +### Construction + +Most consumers obtain an `Agent` via `WebIdDataset.mainSubject` rather than +constructing one directly. To construct one manually: + +```ts +new Agent(term, dataset, factory) +``` + +- `term: NamedNode | BlankNode` — the agent's RDF subject. +- `dataset: DatasetCore` — the dataset to read from. +- `factory: DataFactory` — RDF/JS data factory. + +### Properties + +All properties are read-only getters. Sets are mutable via `@rdfjs/wrapper`'s +`SetFrom` machinery — adding to or deleting from the returned `Set` mutates +the underlying dataset. + +| Property | Type | Predicate | Spec | +| --- | --- | --- | --- | +| `name` | `string \| null` | Computed: `vcard:fn` -> `foaf:name` -> the last URL path segment. | [vCard](https://www.w3.org/TR/vcard-rdf/), [FOAF](http://xmlns.com/foaf/spec/#term_name) | +| `vcardFn` | `string \| undefined` | [`vcard:fn`](https://www.w3.org/TR/vcard-rdf/#fn) | vCard "formatted name" | +| `foafName` | `string \| undefined` | [`foaf:name`](http://xmlns.com/foaf/spec/#term_name) | FOAF | +| `organization` | `string \| null` | [`vcard:organization-name`](https://www.w3.org/TR/vcard-rdf/) | vCard | +| `role` | `string \| null` | [`vcard:role`](https://www.w3.org/TR/vcard-rdf/) | vCard | +| `title` | `string \| null` | [`vcard:title`](https://www.w3.org/TR/vcard-rdf/) | vCard | +| `email` | `string \| null` | Convenience: `hasEmail.value`. | vCard | +| `hasEmail` | `HasValue \| undefined` | [`vcard:hasEmail`](https://www.w3.org/TR/vcard-rdf/#hasEmail). The wrapper resolves `vcard:hasValue` if present (e.g. `mailto:alice@example`). | vCard | +| `phone` | `string \| null` | Convenience: `hasTelephone.value`. | vCard | +| `hasTelephone` | `HasValue \| undefined` | [`vcard:hasTelephone`](https://www.w3.org/TR/vcard-rdf/#hasTelephone). | vCard | +| `website` | `string \| null` | Computed: `vcardHasUrl` -> `foafHomepage`. | vCard / FOAF | +| `vcardHasUrl` | `string \| undefined` | [`vcard:hasURL`](https://www.w3.org/TR/vcard-rdf/) | vCard | +| `foafHomepage` | `string \| undefined` | [`foaf:homepage`](http://xmlns.com/foaf/spec/#term_homepage) | FOAF | +| `photoUrl` | `string \| null` | [`vcard:hasPhoto`](https://www.w3.org/TR/vcard-rdf/) | vCard | +| `pimStorage` | `Set` | [`pim:storage`](http://www.w3.org/ns/pim/space#storage) — the agent's storage roots. | [PIM/space](http://www.w3.org/ns/pim/space) | +| `solidStorage` | `Set` | [`solid:storage`](http://www.w3.org/ns/solid/terms#storage) — Solid Protocol storage. | [Solid Protocol §4.1](https://solidproject.org/TR/protocol#storage) | +| `storageUrls` | `Set` | The union of `pimStorage` and `solidStorage`. | — | +| `knows` | `Set` | [`foaf:knows`](http://xmlns.com/foaf/spec/#term_knows) | FOAF | + +### Example + +```ts +const agent = dataset.mainSubject +if (!agent) throw new Error("WebID profile has no main subject") + +console.log(agent.name) +console.log(agent.email) // null if absent +console.log([...agent.storageUrls]) // string[] of storage roots + +// Add a new storage root by mutating the Set: +agent.pimStorage.add("https://storage.example/alice-photos/") +``` From 56e05b784500b80d86ef6fef78469026b2798c9c Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:31:08 +0100 Subject: [PATCH 4/5] docs: fix accuracy gaps flagged by review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - conversion.md: wacToAcp does not iterate acl:agentClass — only acl:agent and expanded acl:agentGroup members. Documented the limitation so readers do not expect public/authenticated translation for canonical WAC graphs that express those subjects via acl:agentClass. Reworded the bullet that previously implied agent-class translation. - webid.md: clarified that Agent.name's URL fallback strips any trailing #fragment as well as path segments. - solid.md: noted that Resource.mimeType returns the full IANA-vocabulary class IRI rather than the bare media-type string. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/api/conversion.md | 21 +++++++++++++++++---- docs/api/solid.md | 2 +- docs/api/webid.md | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/api/conversion.md b/docs/api/conversion.md index 40c826b..afea8c4 100644 --- a/docs/api/conversion.md +++ b/docs/api/conversion.md @@ -37,15 +37,28 @@ For each `Authorization`: - `acl:default` becomes an `acp:AccessControlResource` with an `acp:memberAccessControl` chain. - Each `acl:mode` is added to the resulting `acp:Policy`'s `acp:allow`. -- Each `acl:agent`, expanded `acl:agentGroup` member, and the - `foaf:Agent` / `acl:AuthenticatedAgent` agent classes is added to a - single `acp:allOf` `acp:Matcher`. `foaf:Agent` maps to - `acp:PublicAgent`; `acl:AuthenticatedAgent` maps to `acp:AuthenticatedAgent`. +- Each `acl:agent` IRI and each `acl:agentGroup` member (`vcard:hasMember`) + is added to a single `acp:allOf` `acp:Matcher`. If an `acl:agent` IRI + happens to be `foaf:Agent` it is rewritten to `acp:PublicAgent`; if it + is `acl:AuthenticatedAgent` it is rewritten to `acp:AuthenticatedAgent`. Throws [`WacToAcpError`](#wactoacperror) when the authorisation includes: - `acl:origin` (no equivalent in ACP). +### Caveats + +- **`acl:agentClass` is not currently translated.** `acl:agentClass` + values (the canonical predicate for `foaf:Agent` "everyone" and + `acl:AuthenticatedAgent` "any signed-in agent" subjects, per + [WAC §5](https://solidproject.org/TR/wac#access-subjects)) are not + iterated by the converter and are silently dropped. WAC documents that + express public or authenticated access through `acl:agentClass` will + produce an ACP graph with no matching `acp:Matcher` for those + categories. The mapping above only fires when those same IRIs appear + as `acl:agent` values (which the test fixture happens to use). Track + in the upstream issue tracker if this matters for your use case. + ## `acpToWac(source, target)` ```ts diff --git a/docs/api/solid.md b/docs/api/solid.md index bdbdec4..0b0d0c9 100644 --- a/docs/api/solid.md +++ b/docs/api/solid.md @@ -48,7 +48,7 @@ new Resource(term, dataset, factory) | `lastModified` | `Date \| undefined` | Computed: `modified` -> `mtime`. | | | `size` | `number \| undefined` | [`posix:size`](http://www.w3.org/ns/posix/stat#size) | | | `type` | `Set` | `rdf:type` | Mutable set. | -| `mimeType` | `string \| undefined` | First member of `type` matching `http://www.w3.org/ns/iana/media-types/.../#Resource`. | The IANA media-type vocabulary used by some Solid servers. | +| `mimeType` | `string \| undefined` | First member of `type` matching `http://www.w3.org/ns/iana/media-types/.../#Resource`. | The full IANA-vocabulary class IRI is returned, not the bare media-type string — extract the media type yourself if you need `image/png` rather than `http://www.w3.org/ns/iana/media-types/image/png#Resource`. | `toString()` returns `id`. diff --git a/docs/api/webid.md b/docs/api/webid.md index 5e5ed8f..578910e 100644 --- a/docs/api/webid.md +++ b/docs/api/webid.md @@ -89,7 +89,7 @@ the underlying dataset. | Property | Type | Predicate | Spec | | --- | --- | --- | --- | -| `name` | `string \| null` | Computed: `vcard:fn` -> `foaf:name` -> the last URL path segment. | [vCard](https://www.w3.org/TR/vcard-rdf/), [FOAF](http://xmlns.com/foaf/spec/#term_name) | +| `name` | `string \| null` | Computed: `vcard:fn` -> `foaf:name` -> the last URL path segment of the WebID IRI with any trailing `#fragment` removed. | [vCard](https://www.w3.org/TR/vcard-rdf/), [FOAF](http://xmlns.com/foaf/spec/#term_name) | | `vcardFn` | `string \| undefined` | [`vcard:fn`](https://www.w3.org/TR/vcard-rdf/#fn) | vCard "formatted name" | | `foafName` | `string \| undefined` | [`foaf:name`](http://xmlns.com/foaf/spec/#term_name) | FOAF | | `organization` | `string \| null` | [`vcard:organization-name`](https://www.w3.org/TR/vcard-rdf/) | vCard | From 9fcca404418afb2e0b786aeb2e10cd33a050560f Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:35:24 +0100 Subject: [PATCH 5/5] docs: address review nits (HasValue, mimeType, phoneType, examples) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README: rewrote the wacToAcp quick-start so the AclResource construction is no longer an inline-comment placeholder; the example now parses a small Turtle fixture into a Store first, mirroring the earlier WAC example. - docs/api/webid.md: dropped the `HasValue` type name (the class is not exported from the package) in favour of an inline shape description for `hasEmail` / `hasTelephone` return values. Reworded the WebIdDataset.mainSubject caveat so it no longer links a source-level TODO; describes the limitation directly instead. - docs/api/solid.md: added a "Known issues" note flagging that Telephone.phoneType uses the `vcard:TelephoneType` IRI as a predicate where the vCard ontology defines that IRI as a class — reflects upstream code as-is and warns consumers. - context7.json: stopped excluding the test/ folder so the worked examples in test/unit/ get indexed alongside the docs. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 15 ++++++++++++--- context7.json | 4 ++-- docs/api/solid.md | 12 +++++++++++- docs/api/webid.md | 17 ++++++++--------- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 22e4c41..b5aa737 100644 --- a/README.md +++ b/README.md @@ -73,15 +73,24 @@ for (const auth of resource.authorizations) { ``` Translate a WAC authorisation into an ACP policy graph (best effort — see -caveats on the conversion docs): +caveats on the [conversion docs](docs/api/conversion.md)): ```ts import { DataFactory, Parser, Store } from "n3" import { AclResource, wacToAcp } from "@solid/object" -const source = new AclResource(/* ... */ new Store(), DataFactory) +const sourceStore = new Store() +sourceStore.addQuads(new Parser().parse(` + @prefix acl: . + [] a acl:Authorization ; + acl:accessTo ; + acl:default ; + acl:agent ; + acl:mode acl:Read, acl:Write . +`)) + const target = new Store() -wacToAcp(source, target) +wacToAcp(new AclResource(sourceStore, DataFactory), target) ``` ## Package layout diff --git a/context7.json b/context7.json index e02681b..9a887b2 100644 --- a/context7.json +++ b/context7.json @@ -4,12 +4,12 @@ "description": "RDF mapping classes for Solid: typed wrappers over RDF/JS datasets covering WebID profiles, Web Access Control (WAC) and Access Control Policy (ACP) resources, Solid containers and resources, vCard email/telephone objects, and the namespaced vocabulary constants those wrappers use.", "folders": [ "docs", - "src" + "src", + "test" ], "excludeFolders": [ "dist", "node_modules", - "test", ".github" ], "excludeFiles": [ diff --git a/docs/api/solid.md b/docs/api/solid.md index 0b0d0c9..cf8e2ce 100644 --- a/docs/api/solid.md +++ b/docs/api/solid.md @@ -149,7 +149,17 @@ Wraps a vCard `Tel` value object — the subject of `vcard:hasTelephone` / | Property | Type | Predicate | | --- | --- | --- | | `phoneNumber` | `string` (read/write) | [`vcard:hasValue`](https://www.w3.org/TR/vcard-rdf/#hasValue) — throws if absent on read. | -| `phoneType` | `string \| undefined` (read/write) | `vcard:TelephoneType` | +| `phoneType` | `string \| undefined` (read/write) | `vcard:TelephoneType` ([known mismatch](#known-issues): the IRI used by the wrapper is a vCard *class*, not a predicate; the corresponding `Email.emailType` getter uses `rdf:type`). | + +### Known issues + +- `Telephone.phoneType` reads and writes triples whose predicate IRI is + `http://www.w3.org/2006/vcard/ns#TelephoneType`, but in the + [vCard ontology](https://www.w3.org/TR/vcard-rdf/) `vcard:TelephoneType` + is a class, not a predicate. The canonical predicate is `rdf:type` + (which is what `Email.emailType` uses). Treat `phoneType` as a + `@solid/object`-internal convention until the upstream fixes the + predicate. ## `TelephoneDataset` diff --git a/docs/api/webid.md b/docs/api/webid.md index 578910e..e8ebeb2 100644 --- a/docs/api/webid.md +++ b/docs/api/webid.md @@ -35,13 +35,12 @@ new WebIdDataset(dataset, factory) | --- | --- | --- | | `mainSubject` | [`Agent`](#agent) \| `undefined` | The first subject in the dataset that has a [`solid:oidcIssuer`](https://solid.github.io/solid-oidc/#discovery) predicate, wrapped as an `Agent`. Returns `undefined` for documents that don't contain an OIDC issuer triple. | -> The current heuristic identifies the WebID by looking for an -> `solid:oidcIssuer` triple. The library author has flagged this as -> incomplete (see the `TODO` in -> [`src/webid/WebIdDataset.ts`](../../src/webid/WebIdDataset.ts)) — a +> The current heuristic identifies the WebID by looking for a +> `solid:oidcIssuer` triple, so profile documents that omit one will +> return `undefined`. A > [`foaf:primaryTopic`](http://xmlns.com/foaf/spec/#term_primaryTopic) > / [`foaf:isPrimaryTopicOf`](http://xmlns.com/foaf/spec/#term_isPrimaryTopicOf) -> based path is planned. +> based fallback is planned upstream. ### Example @@ -95,10 +94,10 @@ the underlying dataset. | `organization` | `string \| null` | [`vcard:organization-name`](https://www.w3.org/TR/vcard-rdf/) | vCard | | `role` | `string \| null` | [`vcard:role`](https://www.w3.org/TR/vcard-rdf/) | vCard | | `title` | `string \| null` | [`vcard:title`](https://www.w3.org/TR/vcard-rdf/) | vCard | -| `email` | `string \| null` | Convenience: `hasEmail.value`. | vCard | -| `hasEmail` | `HasValue \| undefined` | [`vcard:hasEmail`](https://www.w3.org/TR/vcard-rdf/#hasEmail). The wrapper resolves `vcard:hasValue` if present (e.g. `mailto:alice@example`). | vCard | -| `phone` | `string \| null` | Convenience: `hasTelephone.value`. | vCard | -| `hasTelephone` | `HasValue \| undefined` | [`vcard:hasTelephone`](https://www.w3.org/TR/vcard-rdf/#hasTelephone). | vCard | +| `email` | `string \| null` | Convenience: `hasEmail?.value`. | vCard | +| `hasEmail` | term wrapper \| `undefined` | [`vcard:hasEmail`](https://www.w3.org/TR/vcard-rdf/#hasEmail). Returns an opaque term wrapper with a `.value: string` getter that resolves `vcard:hasValue` if present (e.g. `mailto:alice@example`), otherwise the wrapped term's IRI. | vCard | +| `phone` | `string \| null` | Convenience: `hasTelephone?.value`. | vCard | +| `hasTelephone` | term wrapper \| `undefined` | [`vcard:hasTelephone`](https://www.w3.org/TR/vcard-rdf/#hasTelephone). Same opaque shape as `hasEmail`. | vCard | | `website` | `string \| null` | Computed: `vcardHasUrl` -> `foafHomepage`. | vCard / FOAF | | `vcardHasUrl` | `string \| undefined` | [`vcard:hasURL`](https://www.w3.org/TR/vcard-rdf/) | vCard | | `foafHomepage` | `string \| undefined` | [`foaf:homepage`](http://xmlns.com/foaf/spec/#term_homepage) | FOAF |