Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions docs/docs/Infrastructure/eVault.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,7 +65,8 @@ A **MetaEnvelope** is the top-level container for an entity (post, user, message

- **id**: Unique identifier (W3ID). Note: Only IDs registered in the Registry are guaranteed to be globally unique.
- **ontology**: Schema identifier (W3ID, e.g., "550e8400-e29b-41d4-a716-446655440001"). Schema W3IDs can be resolved to their schema definitions via the [Ontology](/docs/Infrastructure/Ontology) service. See [W3DS Basics](/docs/W3DS%20Basics/getting-started) for more information on ontology schemas.
- **acl**: Access Control List (who can access this data)
- **acl**: Legacy access control list (who can access this data)
- **_acl**: The granular access policy — grants, denials, and ontology conditions. Takes precedence over `acl` when present. See [Access Control](/docs/W3DS%20Protocol/Access-Control).
- **envelopes**: Array of individual Envelope nodes

### Envelopes
Expand All@@ -83,7 +84,7 @@ Each field in a MetaEnvelope becomes a separate **Envelope** node in Neo4j:
In Neo4j, the structure looks like:

```cypher
(MetaEnvelope {id, ontology, acl}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
(MetaEnvelope {id, ontology, acl, aclBlock}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
```

This flat graph structure allows:
Expand DownExpand Up@@ -603,26 +604,42 @@ curl -X GET "http://localhost:4000/logs?limit=20&cursor=2025-02-04T12:00:00.000Z

## Access Control

eVault uses **Access Control Lists (ACLs)** to determine who can access data.
A MetaEnvelope can carry access rules two ways. The granular `_acl` policy is the current model; the legacy `acl` array predates it and still works.

### ACL Format
### The `_acl` policy

ACLs are arrays of W3IDs or special values:
`_acl` holds grants (an eName plus a READ/CREATE/UPDATE/DELETE bitmask), denials, and Resource Link Ontology conditions. Decisions run in a fixed order — denials, then the most specific grant, then the ontology groups — and the most specific grant wins without unioning less specific ones.

Full model, wire format, and current limits: [Access Control](/docs/W3DS%20Protocol/Access-Control).

It is stored on the MetaEnvelope node as the `aclBlock` property (JSON), so the policy travels with the record when it syncs. No migration is needed to start using it: the property is optional, and a node without one is read through its legacy array exactly as before.

:::caution Rolling back

Once records begin carrying policies, treat the deployment as forward-only. Earlier builds do not read `aclBlock` and fall back to the `acl` array — which platforms write as `["*"]` — so a record an owner had locked down would become world-readable again on a rollback.

:::

### Legacy ACL format

Arrays of W3IDs or special values:

- `["*"]`: Public read access (anyone can read, but only the eVault owner can write)
- `["@user-a.w3id"]`: Only User A can access (read and write)
- `["@user-a.w3id", "@user-b.w3id"]`: User A and User B can access (read and write)

**Prototype Limitation**: In the current prototype implementation, ACLs provide all-or-nothing access. There is no read-only access withoutwrite access (except for `["*"]` which provides read-only access for everyone). More granular permissions are planned for future versions.
The array is all-or-nothing: there is no read-only-without-write except `["*"]`. That is what `_acl` replaces. A record with an `_acl` block ignores its array entirely; a record without one behaves exactly as it always has.

### Access Enforcement

The Access Guard middleware enforces ACLs:
The Access Guard middleware enforces access on every operation, with the permission the operation needs (read for queries, create/update/delete for the corresponding mutations):

1. **Extract W3ID**: From `X-ENAME` header or [Bearer token](/docs/W3DS%20Protocol/Authentication)
2. **Check ACL**: Verify the requesting W3ID is in the MetaEnvelope's ACL
3. **Filter Results**: Remove ACL field from responses (security)
4. **Allow/Deny**: Grant or deny access based on ACL
2. **Check the policy**: If the record carries `_acl`, decide by it. Otherwise fall back to the legacy array.
3. **Filter Results**: Remove the legacy `acl` array from responses; `_acl` is returned as the policy in force
4. **Allow/Deny**

A valid Registry-issued platform token satisfies the *legacy* path — but it does **not** bypass an `_acl` policy. A record carrying a policy is decided by that policy for every caller.

### Special Cases

Expand Down
209 changes: 209 additions & 0 deletions docs/docs/Post Platform Guide/access-control.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
---
sidebar_position: 6
---

# Implementing Access Control

Your platform writes records into a user's eVault. By default those records are wide open — anything that syncs to a platform can be read by it. This page is how you narrow that.

For the model itself — the bitmask, specificity, the decision order — see [Access Control](/docs/W3DS%20Protocol/Access-Control) in the protocol section. This page is the practical side: what to send, what comes back, and what will bite you.

## What you get if you do nothing

The Web3 Adapter writes every record with `acl: ["*"]`. That means anyone, everything, and it is what all existing platform data looks like today.

Nothing about that changes on its own. Records with no `_acl` block keep behaving exactly as they always have, including the part where any platform holding a valid Registry-issued token can reach them. You opt in per record by sending a policy.

## Setting a policy

`_acl` is an optional field on the same inputs you already use.

```graphql
mutation {
createMetaEnvelope(input: {
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: { content: "…", authorId: "…" }
acl: ["*"]
_acl: {
v: 1
grants: [
{ ename: "@7b9c2e1a-4f30-4c5e-9a21-d8e0f1a2b3c4", perms: 15 }
{ ename: "@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b", perms: 1 }
]
denials: { enames: [], conditions: [] }
default_perms: 0
require: []
}
}) {
metaEnvelope { id }
errors { message }
}
}
```

The owner gets `15` (`0x0F`, everything); one platform gets `1` (`0x01`, read). Nobody else is admitted: `require: []` means no group can pass, so step 3 always refuses.

Send `acl` as well. It is still required by the schema, and it is what any record without a policy falls back to — but where `_acl` is present it is ignored entirely, so its value does not matter.

Available on `createMetaEnvelope`, `storeMetaEnvelope`, `updateMetaEnvelope`, `updateMetaEnvelopeById`, `bulkCreateMetaEnvelopes`, and `uploadFile`.

## Permission values

| Want | `perms` | Hex |
|---|---|---|
| Read only | `1` | `0x01` |
| Read + add, but not edit | `3` | `0x03` |
| Read + edit | `5` | `0x05` |
| Everything | `15` | `0x0F` |

Bits: `1` READ, `2` CREATE, `4` UPDATE, `8` DELETE. Union them.

Two values to avoid sending by accident:

- **`0`** is not "no permissions", it is *no grant at all* — the party falls through to the ontology step as though you had never named them. To actually give someone nothing, leave them out and let the default refuse them.
- **Anything above `15`** is rejected outright. Bits 4–7 are reserved, and a write that sets one fails loudly rather than being quietly narrowed.

## Common shapes

**Owner-only.** Nothing but the owner, no fallback.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

**Public read, owner writes.** The empty group always passes, so anyone reaches `default_perms`.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

This is the closest equivalent of the legacy `["*"]`, except that everyone other than the owner is now read-only rather than able to write.

**Public read, one platform excluded.**

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": ["@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b"], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

A denial beats everything, including a grant to the same party. This is how a user shuts out a platform they do not trust without having to enumerate the ones they do.

**Append-only log.** A collaborator may add entries but never rewrite or remove one.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 },
{ "ename": "@collaborator", "perms": 3 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

## Acting on behalf of a user

Your platform's token proves your platform. It says nothing about which of your users a request is for, which matters as soon as a policy grants anything at user level.

Send the user's eName in `X-ON-BEHALF-OF`:

```http
POST /graphql
Authorization: Bearer <your platform token>
X-ENAME: @<vault owner>
X-ON-BEHALF-OF: @<the user you are acting for>
```

That user becomes the party the policy is evaluated against, and your platform is recorded alongside them — so a user grant applies at user specificity while a grant to your platform still applies at platform specificity. Omit the header and your platform is the party.

Two things to be clear about:

- **It is your assertion, not a proof.** The eVault has no way to check it, so it trusts you. That also means it will let you reach what the user was granted, which may be broader than your own grant. Do not send a user's eName on a request that user did not actually initiate.
- **It will not get you past a denial.** Denials match your platform as well as the asserted user, so a policy that excludes your platform still excludes it whatever name you send.

Only `@`-prefixed eNames count as parties. Anything else is ignored rather than treated as an identity.

## Reading a policy back

`_acl` is a field on `MetaEnvelope`:

```graphql
query {
metaEnvelope(id: "…") {
_acl {
grants { ename perms }
denials { enames }
default_perms
}
}
}
```

You always get the policy actually in force. A record written with only `acl: ["*"]` reports `default_perms: 15` behind an always-passing group rather than returning the array, so you can render one consistent view without caring how the record was written.

## Things that will bite you

**A grant is final.** If your platform is named in `grants`, that grant decides the answer on its own. It never falls through to `default_perms` — so a platform granted `1` on a record whose `default_perms` is `15` has read access, not full access. Being named is not always an upgrade.

**The most specific grant wins outright.** A grant to a user beats one to a platform, and they are not combined. If a record grants your platform `15` and the acting user `1`, a request carrying that user identity gets `1`. The platform's broader grant is not consulted.

**A valid platform token does not open a policied record.** It still works on records with no `_acl`. That bypass is exactly what a policy exists to close, so do not rely on your token to reach data a user has locked down — handle the refusal instead.

**Updates preserve the policy.** An `updateMetaEnvelope` that omits `_acl` leaves the stored policy alone rather than clearing it. To change a policy, send the new one in full — it replaces, it does not merge.

**A policy is visible to everyone who can read the record.** `_acl` is returned, not stripped — so your denial list tells any permitted reader which platforms the user excluded, and your grant list tells them who else has access. Do not put anything in a policy you would not show to its readers.

**Refusals look like two different things.** A record you may not touch raises `Access denied`. A record that does not exist for that eName returns `null`. Do not treat the second as the first — retrying will not help, and neither will asking for a different verb.

## The adapter does not do this yet

`EVaultClient` hardcodes `acl: ["*"]` and has no `_acl` parameter, so records written through `handleChange` cannot carry a policy today. To set one, call the eVault GraphQL endpoint directly for that record.

Everything else about your integration is unchanged — mapping, webhooks, and the Awareness Protocol do not interact with the policy. The policy is stored inside the record, so it travels with the data when it syncs, without your webhook controller doing anything.

## Errors you will get

A malformed policy is rejected whole — nothing is quietly dropped and stored in a weaker form. Every message is prefixed `Invalid _acl:` unless noted.

| Message | Cause |
|---|---|
| `bits 4-7 are reserved and must be 0` | A `perms` or `default_perms` above `15`. |
| `expected an unsigned byte` | `perms` was not an integer in 0-255. |
| `each grant needs an ename` | A grant object missing its `ename`. |
| `unknown operator "…"` | A condition `op` outside `>=`, `>`, `<=`, `<`, `==`. |
| `needs a finite numeric value` | A condition `value` that is not a number. |
| `grants must be an array` (and similar) | A container sent as the wrong shape. |
| `Unsupported _acl version: n` | `v` set to anything but `1`. |

Condition errors name the position — `require[0][1]`, `denials.conditions[0]` — so you can find the offending entry directly.

Two runtime outcomes worth distinguishing, neither of which is a validation error:

- **`Access denied`** — the record exists and the policy refused you. Retrying will not help; asking for a different verb might.
- **`null`** — no record with that id for that `X-ENAME`. Not a permissions problem.

List queries behave differently again: a record you may not read is **omitted from the results**, not reported. So a list can come back shorter than you expect with no error and no indication that anything was withheld. Do not treat a list's length as a count of what exists.

## Not usable yet

Two parts of the protocol document are specified but not connected, and a policy relying on them will not behave as written:

- **Groups.** Group membership is not resolved, so a grant or denial naming a group matches nobody. A group grant simply fails to apply; a **group denial silently fails to deny**, which is the dangerous direction. Name parties individually for now.
- **Ontology conditions.** No evaluator is wired in, so any condition fails. A `require` group containing conditions can never pass, and a deny condition always fires and refuses everyone. Until that lands, use only `grants`, `denials.enames`, and `require: []` or `require: [[]]`.

Enforcement is eVault-side. Platforms and the adapter do not evaluate policies themselves, so do not treat a policy as a reason to skip your own authorization checks.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the protocol model and wire format
- [eVault](/docs/Infrastructure/eVault) — where policies are stored and enforced
- [Webhook Controller](/docs/Post%20Platform%20Guide/webhook-controller) — the inbound side, unaffected by policies
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/ai-agent-skill.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---

# AI Agent Skill
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# eCurrency: Accounts and Ledger MetaEnvelopes
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
---

# Registering a Platform eVault
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
---

# PP Auth demonstrator
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
---

# Authenticating your platform
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,15 @@ The three gates run in order, and each can only narrow the one before it:

A grant cannot widen a certificate. Permitting `health:Read` to a platform never certified for `health` changes nothing.

## Where the record's own rules fit

The policy above is a signed statement about a *subject* — which platforms an owner will deal with at all. It is not stored in the data it protects.

[Access control](/docs/W3DS%20Protocol/Access-Control) is the other half: an `_acl` block inside each record, naming parties and the verbs they hold, plus ontology conditions admitting platforms that were never named. That block is what the eVault evaluates on each request, and it travels with the record when it syncs.

The two are separate gates and neither can widen the other.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the per-record `_acl` policy the eVault enforces
- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
22 changes: 20 additions & 2 deletions docs/docs/W3DS Basics/glossary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,13 @@ Definitions of key terms used across the W3DS and MetaState documentation. Where

## Access

The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by [ACLs](/docs/Infrastructure/eVault#access-control) and [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.
The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by the record's own access policy — [granular access control](/docs/W3DS%20Protocol/Access-Control) or the legacy [ACL](/docs/Infrastructure/eVault#access-control) array — and by [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.

---

## Access Control List (ACL)

The rules stored inside a record saying who may do what with it. Held in the record's `_acl` block as [grants](#grant), [denials](#denial), and ontology conditions, so the rules travel with the data when it syncs rather than living in a table beside it. The older `acl` string array is the same idea without per-verb granularity. See [Access Control](/docs/W3DS%20Protocol/Access-Control).

---

Expand DownExpand Up@@ -38,6 +44,12 @@ A set of data relating to an identifier that is signed by an issuing party (e.g.

---

## Denial

An entry in an [ACL](#access-control-list-acl) that removes access from a party, either by naming its [eName](#web-30-identifier-w3id--ename) or by stating a condition it must clear. A denial overrides any grant — it is the one place where a more specific rule does not win, because deny always does.

---

## eID (ePassport)

A document, similar to X.509, which binds a user's [W3ID](#web-30-identifier-w3id-ename) and the user's [Public Key](#public-key). It is signed by a [digital] notary participating in PKI. See [eID Wallet](/docs/Infrastructure/eID-Wallet) for how the prototype uses eID and key binding.
Expand All@@ -58,7 +70,7 @@ A non-human object within the MetaState such as an organization, a platform, a b

## Envelope

The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition and ACL that defines who is allowed to access it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.
The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition, and its MetaEnvelope carries the access policy that defines who is allowed to do what with it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.

---

Expand All@@ -68,6 +80,12 @@ A secure storage location or server for the management of data and credentials o

---

## Grant

An entry in an [ACL](#access-control-list-acl) pairing a party's [eName](#web-30-identifier-w3id--ename) with the permissions it holds, as a bitmask of Read, Create, Update and Delete. Where several grants could apply, only the most specific is used — a grant to a user beats one to a platform, which beats one to a group — and less specific grants do not add to it.

---

## Group

An [Entity](#entity): a reference to a number of users within the MetaState that holds its own [W3ID](#web-30-identifier-w3id-ename) and [eVault](#evault). It is often seen as a "group" in social networks.
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions docs/docs/Infrastructure/eVault.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,7 +65,8 @@ A **MetaEnvelope** is the top-level container for an entity (post, user, message

- **id**: Unique identifier (W3ID). Note: Only IDs registered in the Registry are guaranteed to be globally unique.
- **ontology**: Schema identifier (W3ID, e.g., "550e8400-e29b-41d4-a716-446655440001"). Schema W3IDs can be resolved to their schema definitions via the [Ontology](/docs/Infrastructure/Ontology) service. See [W3DS Basics](/docs/W3DS%20Basics/getting-started) for more information on ontology schemas.
- **acl**: Access Control List (who can access this data)
- **acl**: Legacy access control list (who can access this data)
- **_acl**: The granular access policy — grants, denials, and ontology conditions. Takes precedence over `acl` when present. See [Access Control](/docs/W3DS%20Protocol/Access-Control).
- **envelopes**: Array of individual Envelope nodes

### Envelopes
Expand All@@ -83,7 +84,7 @@ Each field in a MetaEnvelope becomes a separate **Envelope** node in Neo4j:
In Neo4j, the structure looks like:

```cypher
(MetaEnvelope {id, ontology, acl}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
(MetaEnvelope {id, ontology, acl, aclBlock}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
```

This flat graph structure allows:
Expand DownExpand Up@@ -603,26 +604,42 @@ curl -X GET "http://localhost:4000/logs?limit=20&cursor=2025-02-04T12:00:00.000Z

## Access Control

eVault uses **Access Control Lists (ACLs)** to determine who can access data.
A MetaEnvelope can carry access rules two ways. The granular `_acl` policy is the current model; the legacy `acl` array predates it and still works.

### ACL Format
### The `_acl` policy

ACLs are arrays of W3IDs or special values:
`_acl` holds grants (an eName plus a READ/CREATE/UPDATE/DELETE bitmask), denials, and Resource Link Ontology conditions. Decisions run in a fixed order — denials, then the most specific grant, then the ontology groups — and the most specific grant wins without unioning less specific ones.

Full model, wire format, and current limits: [Access Control](/docs/W3DS%20Protocol/Access-Control).

It is stored on the MetaEnvelope node as the `aclBlock` property (JSON), so the policy travels with the record when it syncs. No migration is needed to start using it: the property is optional, and a node without one is read through its legacy array exactly as before.

:::caution Rolling back

Once records begin carrying policies, treat the deployment as forward-only. Earlier builds do not read `aclBlock` and fall back to the `acl` array — which platforms write as `["*"]` — so a record an owner had locked down would become world-readable again on a rollback.

:::

### Legacy ACL format

Arrays of W3IDs or special values:

- `["*"]`: Public read access (anyone can read, but only the eVault owner can write)
- `["@user-a.w3id"]`: Only User A can access (read and write)
- `["@user-a.w3id", "@user-b.w3id"]`: User A and User B can access (read and write)

**Prototype Limitation**: In the current prototype implementation, ACLs provide all-or-nothing access. There is no read-only access withoutwrite access (except for `["*"]` which provides read-only access for everyone). More granular permissions are planned for future versions.
The array is all-or-nothing: there is no read-only-without-write except `["*"]`. That is what `_acl` replaces. A record with an `_acl` block ignores its array entirely; a record without one behaves exactly as it always has.

### Access Enforcement

The Access Guard middleware enforces ACLs:
The Access Guard middleware enforces access on every operation, with the permission the operation needs (read for queries, create/update/delete for the corresponding mutations):

1. **Extract W3ID**: From `X-ENAME` header or [Bearer token](/docs/W3DS%20Protocol/Authentication)
2. **Check ACL**: Verify the requesting W3ID is in the MetaEnvelope's ACL
3. **Filter Results**: Remove ACL field from responses (security)
4. **Allow/Deny**: Grant or deny access based on ACL
2. **Check the policy**: If the record carries `_acl`, decide by it. Otherwise fall back to the legacy array.
3. **Filter Results**: Remove the legacy `acl` array from responses; `_acl` is returned as the policy in force
4. **Allow/Deny**

A valid Registry-issued platform token satisfies the *legacy* path — but it does **not** bypass an `_acl` policy. A record carrying a policy is decided by that policy for every caller.

### Special Cases

Expand Down
209 changes: 209 additions & 0 deletions docs/docs/Post Platform Guide/access-control.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
---
sidebar_position: 6
---

# Implementing Access Control

Your platform writes records into a user's eVault. By default those records are wide open — anything that syncs to a platform can be read by it. This page is how you narrow that.

For the model itself — the bitmask, specificity, the decision order — see [Access Control](/docs/W3DS%20Protocol/Access-Control) in the protocol section. This page is the practical side: what to send, what comes back, and what will bite you.

## What you get if you do nothing

The Web3 Adapter writes every record with `acl: ["*"]`. That means anyone, everything, and it is what all existing platform data looks like today.

Nothing about that changes on its own. Records with no `_acl` block keep behaving exactly as they always have, including the part where any platform holding a valid Registry-issued token can reach them. You opt in per record by sending a policy.

## Setting a policy

`_acl` is an optional field on the same inputs you already use.

```graphql
mutation {
createMetaEnvelope(input: {
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: { content: "…", authorId: "…" }
acl: ["*"]
_acl: {
v: 1
grants: [
{ ename: "@7b9c2e1a-4f30-4c5e-9a21-d8e0f1a2b3c4", perms: 15 }
{ ename: "@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b", perms: 1 }
]
denials: { enames: [], conditions: [] }
default_perms: 0
require: []
}
}) {
metaEnvelope { id }
errors { message }
}
}
```

The owner gets `15` (`0x0F`, everything); one platform gets `1` (`0x01`, read). Nobody else is admitted: `require: []` means no group can pass, so step 3 always refuses.

Send `acl` as well. It is still required by the schema, and it is what any record without a policy falls back to — but where `_acl` is present it is ignored entirely, so its value does not matter.

Available on `createMetaEnvelope`, `storeMetaEnvelope`, `updateMetaEnvelope`, `updateMetaEnvelopeById`, `bulkCreateMetaEnvelopes`, and `uploadFile`.

## Permission values

| Want | `perms` | Hex |
|---|---|---|
| Read only | `1` | `0x01` |
| Read + add, but not edit | `3` | `0x03` |
| Read + edit | `5` | `0x05` |
| Everything | `15` | `0x0F` |

Bits: `1` READ, `2` CREATE, `4` UPDATE, `8` DELETE. Union them.

Two values to avoid sending by accident:

- **`0`** is not "no permissions", it is *no grant at all* — the party falls through to the ontology step as though you had never named them. To actually give someone nothing, leave them out and let the default refuse them.
- **Anything above `15`** is rejected outright. Bits 4–7 are reserved, and a write that sets one fails loudly rather than being quietly narrowed.

## Common shapes

**Owner-only.** Nothing but the owner, no fallback.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

**Public read, owner writes.** The empty group always passes, so anyone reaches `default_perms`.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

This is the closest equivalent of the legacy `["*"]`, except that everyone other than the owner is now read-only rather than able to write.

**Public read, one platform excluded.**

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": ["@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b"], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

A denial beats everything, including a grant to the same party. This is how a user shuts out a platform they do not trust without having to enumerate the ones they do.

**Append-only log.** A collaborator may add entries but never rewrite or remove one.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 },
{ "ename": "@collaborator", "perms": 3 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

## Acting on behalf of a user

Your platform's token proves your platform. It says nothing about which of your users a request is for, which matters as soon as a policy grants anything at user level.

Send the user's eName in `X-ON-BEHALF-OF`:

```http
POST /graphql
Authorization: Bearer <your platform token>
X-ENAME: @<vault owner>
X-ON-BEHALF-OF: @<the user you are acting for>
```

That user becomes the party the policy is evaluated against, and your platform is recorded alongside them — so a user grant applies at user specificity while a grant to your platform still applies at platform specificity. Omit the header and your platform is the party.

Two things to be clear about:

- **It is your assertion, not a proof.** The eVault has no way to check it, so it trusts you. That also means it will let you reach what the user was granted, which may be broader than your own grant. Do not send a user's eName on a request that user did not actually initiate.
- **It will not get you past a denial.** Denials match your platform as well as the asserted user, so a policy that excludes your platform still excludes it whatever name you send.

Only `@`-prefixed eNames count as parties. Anything else is ignored rather than treated as an identity.

## Reading a policy back

`_acl` is a field on `MetaEnvelope`:

```graphql
query {
metaEnvelope(id: "…") {
_acl {
grants { ename perms }
denials { enames }
default_perms
}
}
}
```

You always get the policy actually in force. A record written with only `acl: ["*"]` reports `default_perms: 15` behind an always-passing group rather than returning the array, so you can render one consistent view without caring how the record was written.

## Things that will bite you

**A grant is final.** If your platform is named in `grants`, that grant decides the answer on its own. It never falls through to `default_perms` — so a platform granted `1` on a record whose `default_perms` is `15` has read access, not full access. Being named is not always an upgrade.

**The most specific grant wins outright.** A grant to a user beats one to a platform, and they are not combined. If a record grants your platform `15` and the acting user `1`, a request carrying that user identity gets `1`. The platform's broader grant is not consulted.

**A valid platform token does not open a policied record.** It still works on records with no `_acl`. That bypass is exactly what a policy exists to close, so do not rely on your token to reach data a user has locked down — handle the refusal instead.

**Updates preserve the policy.** An `updateMetaEnvelope` that omits `_acl` leaves the stored policy alone rather than clearing it. To change a policy, send the new one in full — it replaces, it does not merge.

**A policy is visible to everyone who can read the record.** `_acl` is returned, not stripped — so your denial list tells any permitted reader which platforms the user excluded, and your grant list tells them who else has access. Do not put anything in a policy you would not show to its readers.

**Refusals look like two different things.** A record you may not touch raises `Access denied`. A record that does not exist for that eName returns `null`. Do not treat the second as the first — retrying will not help, and neither will asking for a different verb.

## The adapter does not do this yet

`EVaultClient` hardcodes `acl: ["*"]` and has no `_acl` parameter, so records written through `handleChange` cannot carry a policy today. To set one, call the eVault GraphQL endpoint directly for that record.

Everything else about your integration is unchanged — mapping, webhooks, and the Awareness Protocol do not interact with the policy. The policy is stored inside the record, so it travels with the data when it syncs, without your webhook controller doing anything.

## Errors you will get

A malformed policy is rejected whole — nothing is quietly dropped and stored in a weaker form. Every message is prefixed `Invalid _acl:` unless noted.

| Message | Cause |
|---|---|
| `bits 4-7 are reserved and must be 0` | A `perms` or `default_perms` above `15`. |
| `expected an unsigned byte` | `perms` was not an integer in 0-255. |
| `each grant needs an ename` | A grant object missing its `ename`. |
| `unknown operator "…"` | A condition `op` outside `>=`, `>`, `<=`, `<`, `==`. |
| `needs a finite numeric value` | A condition `value` that is not a number. |
| `grants must be an array` (and similar) | A container sent as the wrong shape. |
| `Unsupported _acl version: n` | `v` set to anything but `1`. |

Condition errors name the position — `require[0][1]`, `denials.conditions[0]` — so you can find the offending entry directly.

Two runtime outcomes worth distinguishing, neither of which is a validation error:

- **`Access denied`** — the record exists and the policy refused you. Retrying will not help; asking for a different verb might.
- **`null`** — no record with that id for that `X-ENAME`. Not a permissions problem.

List queries behave differently again: a record you may not read is **omitted from the results**, not reported. So a list can come back shorter than you expect with no error and no indication that anything was withheld. Do not treat a list's length as a count of what exists.

## Not usable yet

Two parts of the protocol document are specified but not connected, and a policy relying on them will not behave as written:

- **Groups.** Group membership is not resolved, so a grant or denial naming a group matches nobody. A group grant simply fails to apply; a **group denial silently fails to deny**, which is the dangerous direction. Name parties individually for now.
- **Ontology conditions.** No evaluator is wired in, so any condition fails. A `require` group containing conditions can never pass, and a deny condition always fires and refuses everyone. Until that lands, use only `grants`, `denials.enames`, and `require: []` or `require: [[]]`.

Enforcement is eVault-side. Platforms and the adapter do not evaluate policies themselves, so do not treat a policy as a reason to skip your own authorization checks.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the protocol model and wire format
- [eVault](/docs/Infrastructure/eVault) — where policies are stored and enforced
- [Webhook Controller](/docs/Post%20Platform%20Guide/webhook-controller) — the inbound side, unaffected by policies
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/ai-agent-skill.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---

# AI Agent Skill
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# eCurrency: Accounts and Ledger MetaEnvelopes
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
---

# Registering a Platform eVault
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
---

# PP Auth demonstrator
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
---

# Authenticating your platform
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,15 @@ The three gates run in order, and each can only narrow the one before it:

A grant cannot widen a certificate. Permitting `health:Read` to a platform never certified for `health` changes nothing.

## Where the record's own rules fit

The policy above is a signed statement about a *subject* — which platforms an owner will deal with at all. It is not stored in the data it protects.

[Access control](/docs/W3DS%20Protocol/Access-Control) is the other half: an `_acl` block inside each record, naming parties and the verbs they hold, plus ontology conditions admitting platforms that were never named. That block is what the eVault evaluates on each request, and it travels with the record when it syncs.

The two are separate gates and neither can widen the other.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the per-record `_acl` policy the eVault enforces
- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
22 changes: 20 additions & 2 deletions docs/docs/W3DS Basics/glossary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,13 @@ Definitions of key terms used across the W3DS and MetaState documentation. Where

## Access

The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by [ACLs](/docs/Infrastructure/eVault#access-control) and [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.
The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by the record's own access policy — [granular access control](/docs/W3DS%20Protocol/Access-Control) or the legacy [ACL](/docs/Infrastructure/eVault#access-control) array — and by [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.

---

## Access Control List (ACL)

The rules stored inside a record saying who may do what with it. Held in the record's `_acl` block as [grants](#grant), [denials](#denial), and ontology conditions, so the rules travel with the data when it syncs rather than living in a table beside it. The older `acl` string array is the same idea without per-verb granularity. See [Access Control](/docs/W3DS%20Protocol/Access-Control).

---

Expand DownExpand Up@@ -38,6 +44,12 @@ A set of data relating to an identifier that is signed by an issuing party (e.g.

---

## Denial

An entry in an [ACL](#access-control-list-acl) that removes access from a party, either by naming its [eName](#web-30-identifier-w3id--ename) or by stating a condition it must clear. A denial overrides any grant — it is the one place where a more specific rule does not win, because deny always does.

---

## eID (ePassport)

A document, similar to X.509, which binds a user's [W3ID](#web-30-identifier-w3id-ename) and the user's [Public Key](#public-key). It is signed by a [digital] notary participating in PKI. See [eID Wallet](/docs/Infrastructure/eID-Wallet) for how the prototype uses eID and key binding.
Expand All@@ -58,7 +70,7 @@ A non-human object within the MetaState such as an organization, a platform, a b

## Envelope

The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition and ACL that defines who is allowed to access it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.
The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition, and its MetaEnvelope carries the access policy that defines who is allowed to do what with it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.

---

Expand All@@ -68,6 +80,12 @@ A secure storage location or server for the management of data and credentials o

---

## Grant

An entry in an [ACL](#access-control-list-acl) pairing a party's [eName](#web-30-identifier-w3id--ename) with the permissions it holds, as a bitmask of Read, Create, Update and Delete. Where several grants could apply, only the most specific is used — a grant to a user beats one to a platform, which beats one to a group — and less specific grants do not add to it.

---

## Group

An [Entity](#entity): a reference to a number of users within the MetaState that holds its own [W3ID](#web-30-identifier-w3id-ename) and [eVault](#evault). It is often seen as a "group" in social networks.
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions docs/docs/Infrastructure/eVault.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,7 +65,8 @@ A **MetaEnvelope** is the top-level container for an entity (post, user, message

- **id**: Unique identifier (W3ID). Note: Only IDs registered in the Registry are guaranteed to be globally unique.
- **ontology**: Schema identifier (W3ID, e.g., "550e8400-e29b-41d4-a716-446655440001"). Schema W3IDs can be resolved to their schema definitions via the [Ontology](/docs/Infrastructure/Ontology) service. See [W3DS Basics](/docs/W3DS%20Basics/getting-started) for more information on ontology schemas.
- **acl**: Access Control List (who can access this data)
- **acl**: Legacy access control list (who can access this data)
- **_acl**: The granular access policy — grants, denials, and ontology conditions. Takes precedence over `acl` when present. See [Access Control](/docs/W3DS%20Protocol/Access-Control).
- **envelopes**: Array of individual Envelope nodes

### Envelopes
Expand All@@ -83,7 +84,7 @@ Each field in a MetaEnvelope becomes a separate **Envelope** node in Neo4j:
In Neo4j, the structure looks like:

```cypher
(MetaEnvelope {id, ontology, acl}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
(MetaEnvelope {id, ontology, acl, aclBlock}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
```

This flat graph structure allows:
Expand DownExpand Up@@ -603,26 +604,42 @@ curl -X GET "http://localhost:4000/logs?limit=20&cursor=2025-02-04T12:00:00.000Z

## Access Control

eVault uses **Access Control Lists (ACLs)** to determine who can access data.
A MetaEnvelope can carry access rules two ways. The granular `_acl` policy is the current model; the legacy `acl` array predates it and still works.

### ACL Format
### The `_acl` policy

ACLs are arrays of W3IDs or special values:
`_acl` holds grants (an eName plus a READ/CREATE/UPDATE/DELETE bitmask), denials, and Resource Link Ontology conditions. Decisions run in a fixed order — denials, then the most specific grant, then the ontology groups — and the most specific grant wins without unioning less specific ones.

Full model, wire format, and current limits: [Access Control](/docs/W3DS%20Protocol/Access-Control).

It is stored on the MetaEnvelope node as the `aclBlock` property (JSON), so the policy travels with the record when it syncs. No migration is needed to start using it: the property is optional, and a node without one is read through its legacy array exactly as before.

:::caution Rolling back

Once records begin carrying policies, treat the deployment as forward-only. Earlier builds do not read `aclBlock` and fall back to the `acl` array — which platforms write as `["*"]` — so a record an owner had locked down would become world-readable again on a rollback.

:::

### Legacy ACL format

Arrays of W3IDs or special values:

- `["*"]`: Public read access (anyone can read, but only the eVault owner can write)
- `["@user-a.w3id"]`: Only User A can access (read and write)
- `["@user-a.w3id", "@user-b.w3id"]`: User A and User B can access (read and write)

**Prototype Limitation**: In the current prototype implementation, ACLs provide all-or-nothing access. There is no read-only access withoutwrite access (except for `["*"]` which provides read-only access for everyone). More granular permissions are planned for future versions.
The array is all-or-nothing: there is no read-only-without-write except `["*"]`. That is what `_acl` replaces. A record with an `_acl` block ignores its array entirely; a record without one behaves exactly as it always has.

### Access Enforcement

The Access Guard middleware enforces ACLs:
The Access Guard middleware enforces access on every operation, with the permission the operation needs (read for queries, create/update/delete for the corresponding mutations):

1. **Extract W3ID**: From `X-ENAME` header or [Bearer token](/docs/W3DS%20Protocol/Authentication)
2. **Check ACL**: Verify the requesting W3ID is in the MetaEnvelope's ACL
3. **Filter Results**: Remove ACL field from responses (security)
4. **Allow/Deny**: Grant or deny access based on ACL
2. **Check the policy**: If the record carries `_acl`, decide by it. Otherwise fall back to the legacy array.
3. **Filter Results**: Remove the legacy `acl` array from responses; `_acl` is returned as the policy in force
4. **Allow/Deny**

A valid Registry-issued platform token satisfies the *legacy* path — but it does **not** bypass an `_acl` policy. A record carrying a policy is decided by that policy for every caller.

### Special Cases

Expand Down
209 changes: 209 additions & 0 deletions docs/docs/Post Platform Guide/access-control.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
---
sidebar_position: 6
---

# Implementing Access Control

Your platform writes records into a user's eVault. By default those records are wide open — anything that syncs to a platform can be read by it. This page is how you narrow that.

For the model itself — the bitmask, specificity, the decision order — see [Access Control](/docs/W3DS%20Protocol/Access-Control) in the protocol section. This page is the practical side: what to send, what comes back, and what will bite you.

## What you get if you do nothing

The Web3 Adapter writes every record with `acl: ["*"]`. That means anyone, everything, and it is what all existing platform data looks like today.

Nothing about that changes on its own. Records with no `_acl` block keep behaving exactly as they always have, including the part where any platform holding a valid Registry-issued token can reach them. You opt in per record by sending a policy.

## Setting a policy

`_acl` is an optional field on the same inputs you already use.

```graphql
mutation {
createMetaEnvelope(input: {
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: { content: "…", authorId: "…" }
acl: ["*"]
_acl: {
v: 1
grants: [
{ ename: "@7b9c2e1a-4f30-4c5e-9a21-d8e0f1a2b3c4", perms: 15 }
{ ename: "@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b", perms: 1 }
]
denials: { enames: [], conditions: [] }
default_perms: 0
require: []
}
}) {
metaEnvelope { id }
errors { message }
}
}
```

The owner gets `15` (`0x0F`, everything); one platform gets `1` (`0x01`, read). Nobody else is admitted: `require: []` means no group can pass, so step 3 always refuses.

Send `acl` as well. It is still required by the schema, and it is what any record without a policy falls back to — but where `_acl` is present it is ignored entirely, so its value does not matter.

Available on `createMetaEnvelope`, `storeMetaEnvelope`, `updateMetaEnvelope`, `updateMetaEnvelopeById`, `bulkCreateMetaEnvelopes`, and `uploadFile`.

## Permission values

| Want | `perms` | Hex |
|---|---|---|
| Read only | `1` | `0x01` |
| Read + add, but not edit | `3` | `0x03` |
| Read + edit | `5` | `0x05` |
| Everything | `15` | `0x0F` |

Bits: `1` READ, `2` CREATE, `4` UPDATE, `8` DELETE. Union them.

Two values to avoid sending by accident:

- **`0`** is not "no permissions", it is *no grant at all* — the party falls through to the ontology step as though you had never named them. To actually give someone nothing, leave them out and let the default refuse them.
- **Anything above `15`** is rejected outright. Bits 4–7 are reserved, and a write that sets one fails loudly rather than being quietly narrowed.

## Common shapes

**Owner-only.** Nothing but the owner, no fallback.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

**Public read, owner writes.** The empty group always passes, so anyone reaches `default_perms`.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

This is the closest equivalent of the legacy `["*"]`, except that everyone other than the owner is now read-only rather than able to write.

**Public read, one platform excluded.**

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": ["@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b"], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

A denial beats everything, including a grant to the same party. This is how a user shuts out a platform they do not trust without having to enumerate the ones they do.

**Append-only log.** A collaborator may add entries but never rewrite or remove one.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 },
{ "ename": "@collaborator", "perms": 3 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

## Acting on behalf of a user

Your platform's token proves your platform. It says nothing about which of your users a request is for, which matters as soon as a policy grants anything at user level.

Send the user's eName in `X-ON-BEHALF-OF`:

```http
POST /graphql
Authorization: Bearer <your platform token>
X-ENAME: @<vault owner>
X-ON-BEHALF-OF: @<the user you are acting for>
```

That user becomes the party the policy is evaluated against, and your platform is recorded alongside them — so a user grant applies at user specificity while a grant to your platform still applies at platform specificity. Omit the header and your platform is the party.

Two things to be clear about:

- **It is your assertion, not a proof.** The eVault has no way to check it, so it trusts you. That also means it will let you reach what the user was granted, which may be broader than your own grant. Do not send a user's eName on a request that user did not actually initiate.
- **It will not get you past a denial.** Denials match your platform as well as the asserted user, so a policy that excludes your platform still excludes it whatever name you send.

Only `@`-prefixed eNames count as parties. Anything else is ignored rather than treated as an identity.

## Reading a policy back

`_acl` is a field on `MetaEnvelope`:

```graphql
query {
metaEnvelope(id: "…") {
_acl {
grants { ename perms }
denials { enames }
default_perms
}
}
}
```

You always get the policy actually in force. A record written with only `acl: ["*"]` reports `default_perms: 15` behind an always-passing group rather than returning the array, so you can render one consistent view without caring how the record was written.

## Things that will bite you

**A grant is final.** If your platform is named in `grants`, that grant decides the answer on its own. It never falls through to `default_perms` — so a platform granted `1` on a record whose `default_perms` is `15` has read access, not full access. Being named is not always an upgrade.

**The most specific grant wins outright.** A grant to a user beats one to a platform, and they are not combined. If a record grants your platform `15` and the acting user `1`, a request carrying that user identity gets `1`. The platform's broader grant is not consulted.

**A valid platform token does not open a policied record.** It still works on records with no `_acl`. That bypass is exactly what a policy exists to close, so do not rely on your token to reach data a user has locked down — handle the refusal instead.

**Updates preserve the policy.** An `updateMetaEnvelope` that omits `_acl` leaves the stored policy alone rather than clearing it. To change a policy, send the new one in full — it replaces, it does not merge.

**A policy is visible to everyone who can read the record.** `_acl` is returned, not stripped — so your denial list tells any permitted reader which platforms the user excluded, and your grant list tells them who else has access. Do not put anything in a policy you would not show to its readers.

**Refusals look like two different things.** A record you may not touch raises `Access denied`. A record that does not exist for that eName returns `null`. Do not treat the second as the first — retrying will not help, and neither will asking for a different verb.

## The adapter does not do this yet

`EVaultClient` hardcodes `acl: ["*"]` and has no `_acl` parameter, so records written through `handleChange` cannot carry a policy today. To set one, call the eVault GraphQL endpoint directly for that record.

Everything else about your integration is unchanged — mapping, webhooks, and the Awareness Protocol do not interact with the policy. The policy is stored inside the record, so it travels with the data when it syncs, without your webhook controller doing anything.

## Errors you will get

A malformed policy is rejected whole — nothing is quietly dropped and stored in a weaker form. Every message is prefixed `Invalid _acl:` unless noted.

| Message | Cause |
|---|---|
| `bits 4-7 are reserved and must be 0` | A `perms` or `default_perms` above `15`. |
| `expected an unsigned byte` | `perms` was not an integer in 0-255. |
| `each grant needs an ename` | A grant object missing its `ename`. |
| `unknown operator "…"` | A condition `op` outside `>=`, `>`, `<=`, `<`, `==`. |
| `needs a finite numeric value` | A condition `value` that is not a number. |
| `grants must be an array` (and similar) | A container sent as the wrong shape. |
| `Unsupported _acl version: n` | `v` set to anything but `1`. |

Condition errors name the position — `require[0][1]`, `denials.conditions[0]` — so you can find the offending entry directly.

Two runtime outcomes worth distinguishing, neither of which is a validation error:

- **`Access denied`** — the record exists and the policy refused you. Retrying will not help; asking for a different verb might.
- **`null`** — no record with that id for that `X-ENAME`. Not a permissions problem.

List queries behave differently again: a record you may not read is **omitted from the results**, not reported. So a list can come back shorter than you expect with no error and no indication that anything was withheld. Do not treat a list's length as a count of what exists.

## Not usable yet

Two parts of the protocol document are specified but not connected, and a policy relying on them will not behave as written:

- **Groups.** Group membership is not resolved, so a grant or denial naming a group matches nobody. A group grant simply fails to apply; a **group denial silently fails to deny**, which is the dangerous direction. Name parties individually for now.
- **Ontology conditions.** No evaluator is wired in, so any condition fails. A `require` group containing conditions can never pass, and a deny condition always fires and refuses everyone. Until that lands, use only `grants`, `denials.enames`, and `require: []` or `require: [[]]`.

Enforcement is eVault-side. Platforms and the adapter do not evaluate policies themselves, so do not treat a policy as a reason to skip your own authorization checks.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the protocol model and wire format
- [eVault](/docs/Infrastructure/eVault) — where policies are stored and enforced
- [Webhook Controller](/docs/Post%20Platform%20Guide/webhook-controller) — the inbound side, unaffected by policies
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/ai-agent-skill.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---

# AI Agent Skill
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# eCurrency: Accounts and Ledger MetaEnvelopes
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
---

# Registering a Platform eVault
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
---

# PP Auth demonstrator
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
---

# Authenticating your platform
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,15 @@ The three gates run in order, and each can only narrow the one before it:

A grant cannot widen a certificate. Permitting `health:Read` to a platform never certified for `health` changes nothing.

## Where the record's own rules fit

The policy above is a signed statement about a *subject* — which platforms an owner will deal with at all. It is not stored in the data it protects.

[Access control](/docs/W3DS%20Protocol/Access-Control) is the other half: an `_acl` block inside each record, naming parties and the verbs they hold, plus ontology conditions admitting platforms that were never named. That block is what the eVault evaluates on each request, and it travels with the record when it syncs.

The two are separate gates and neither can widen the other.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the per-record `_acl` policy the eVault enforces
- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
22 changes: 20 additions & 2 deletions docs/docs/W3DS Basics/glossary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,13 @@ Definitions of key terms used across the W3DS and MetaState documentation. Where

## Access

The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by [ACLs](/docs/Infrastructure/eVault#access-control) and [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.
The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by the record's own access policy — [granular access control](/docs/W3DS%20Protocol/Access-Control) or the legacy [ACL](/docs/Infrastructure/eVault#access-control) array — and by [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.

---

## Access Control List (ACL)

The rules stored inside a record saying who may do what with it. Held in the record's `_acl` block as [grants](#grant), [denials](#denial), and ontology conditions, so the rules travel with the data when it syncs rather than living in a table beside it. The older `acl` string array is the same idea without per-verb granularity. See [Access Control](/docs/W3DS%20Protocol/Access-Control).

---

Expand DownExpand Up@@ -38,6 +44,12 @@ A set of data relating to an identifier that is signed by an issuing party (e.g.

---

## Denial

An entry in an [ACL](#access-control-list-acl) that removes access from a party, either by naming its [eName](#web-30-identifier-w3id--ename) or by stating a condition it must clear. A denial overrides any grant — it is the one place where a more specific rule does not win, because deny always does.

---

## eID (ePassport)

A document, similar to X.509, which binds a user's [W3ID](#web-30-identifier-w3id-ename) and the user's [Public Key](#public-key). It is signed by a [digital] notary participating in PKI. See [eID Wallet](/docs/Infrastructure/eID-Wallet) for how the prototype uses eID and key binding.
Expand All@@ -58,7 +70,7 @@ A non-human object within the MetaState such as an organization, a platform, a b

## Envelope

The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition and ACL that defines who is allowed to access it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.
The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition, and its MetaEnvelope carries the access policy that defines who is allowed to do what with it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.

---

Expand All@@ -68,6 +80,12 @@ A secure storage location or server for the management of data and credentials o

---

## Grant

An entry in an [ACL](#access-control-list-acl) pairing a party's [eName](#web-30-identifier-w3id--ename) with the permissions it holds, as a bitmask of Read, Create, Update and Delete. Where several grants could apply, only the most specific is used — a grant to a user beats one to a platform, which beats one to a group — and less specific grants do not add to it.

---

## Group

An [Entity](#entity): a reference to a number of users within the MetaState that holds its own [W3ID](#web-30-identifier-w3id-ename) and [eVault](#evault). It is often seen as a "group" in social networks.
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions docs/docs/Infrastructure/eVault.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,7 +65,8 @@ A **MetaEnvelope** is the top-level container for an entity (post, user, message

- **id**: Unique identifier (W3ID). Note: Only IDs registered in the Registry are guaranteed to be globally unique.
- **ontology**: Schema identifier (W3ID, e.g., "550e8400-e29b-41d4-a716-446655440001"). Schema W3IDs can be resolved to their schema definitions via the [Ontology](/docs/Infrastructure/Ontology) service. See [W3DS Basics](/docs/W3DS%20Basics/getting-started) for more information on ontology schemas.
- **acl**: Access Control List (who can access this data)
- **acl**: Legacy access control list (who can access this data)
- **_acl**: The granular access policy — grants, denials, and ontology conditions. Takes precedence over `acl` when present. See [Access Control](/docs/W3DS%20Protocol/Access-Control).
- **envelopes**: Array of individual Envelope nodes

### Envelopes
Expand All@@ -83,7 +84,7 @@ Each field in a MetaEnvelope becomes a separate **Envelope** node in Neo4j:
In Neo4j, the structure looks like:

```cypher
(MetaEnvelope {id, ontology, acl}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
(MetaEnvelope {id, ontology, acl, aclBlock}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
```

This flat graph structure allows:
Expand DownExpand Up@@ -603,26 +604,42 @@ curl -X GET "http://localhost:4000/logs?limit=20&cursor=2025-02-04T12:00:00.000Z

## Access Control

eVault uses **Access Control Lists (ACLs)** to determine who can access data.
A MetaEnvelope can carry access rules two ways. The granular `_acl` policy is the current model; the legacy `acl` array predates it and still works.

### ACL Format
### The `_acl` policy

ACLs are arrays of W3IDs or special values:
`_acl` holds grants (an eName plus a READ/CREATE/UPDATE/DELETE bitmask), denials, and Resource Link Ontology conditions. Decisions run in a fixed order — denials, then the most specific grant, then the ontology groups — and the most specific grant wins without unioning less specific ones.

Full model, wire format, and current limits: [Access Control](/docs/W3DS%20Protocol/Access-Control).

It is stored on the MetaEnvelope node as the `aclBlock` property (JSON), so the policy travels with the record when it syncs. No migration is needed to start using it: the property is optional, and a node without one is read through its legacy array exactly as before.

:::caution Rolling back

Once records begin carrying policies, treat the deployment as forward-only. Earlier builds do not read `aclBlock` and fall back to the `acl` array — which platforms write as `["*"]` — so a record an owner had locked down would become world-readable again on a rollback.

:::

### Legacy ACL format

Arrays of W3IDs or special values:

- `["*"]`: Public read access (anyone can read, but only the eVault owner can write)
- `["@user-a.w3id"]`: Only User A can access (read and write)
- `["@user-a.w3id", "@user-b.w3id"]`: User A and User B can access (read and write)

**Prototype Limitation**: In the current prototype implementation, ACLs provide all-or-nothing access. There is no read-only access withoutwrite access (except for `["*"]` which provides read-only access for everyone). More granular permissions are planned for future versions.
The array is all-or-nothing: there is no read-only-without-write except `["*"]`. That is what `_acl` replaces. A record with an `_acl` block ignores its array entirely; a record without one behaves exactly as it always has.

### Access Enforcement

The Access Guard middleware enforces ACLs:
The Access Guard middleware enforces access on every operation, with the permission the operation needs (read for queries, create/update/delete for the corresponding mutations):

1. **Extract W3ID**: From `X-ENAME` header or [Bearer token](/docs/W3DS%20Protocol/Authentication)
2. **Check ACL**: Verify the requesting W3ID is in the MetaEnvelope's ACL
3. **Filter Results**: Remove ACL field from responses (security)
4. **Allow/Deny**: Grant or deny access based on ACL
2. **Check the policy**: If the record carries `_acl`, decide by it. Otherwise fall back to the legacy array.
3. **Filter Results**: Remove the legacy `acl` array from responses; `_acl` is returned as the policy in force
4. **Allow/Deny**

A valid Registry-issued platform token satisfies the *legacy* path — but it does **not** bypass an `_acl` policy. A record carrying a policy is decided by that policy for every caller.

### Special Cases

Expand Down
209 changes: 209 additions & 0 deletions docs/docs/Post Platform Guide/access-control.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
---
sidebar_position: 6
---

# Implementing Access Control

Your platform writes records into a user's eVault. By default those records are wide open — anything that syncs to a platform can be read by it. This page is how you narrow that.

For the model itself — the bitmask, specificity, the decision order — see [Access Control](/docs/W3DS%20Protocol/Access-Control) in the protocol section. This page is the practical side: what to send, what comes back, and what will bite you.

## What you get if you do nothing

The Web3 Adapter writes every record with `acl: ["*"]`. That means anyone, everything, and it is what all existing platform data looks like today.

Nothing about that changes on its own. Records with no `_acl` block keep behaving exactly as they always have, including the part where any platform holding a valid Registry-issued token can reach them. You opt in per record by sending a policy.

## Setting a policy

`_acl` is an optional field on the same inputs you already use.

```graphql
mutation {
createMetaEnvelope(input: {
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: { content: "…", authorId: "…" }
acl: ["*"]
_acl: {
v: 1
grants: [
{ ename: "@7b9c2e1a-4f30-4c5e-9a21-d8e0f1a2b3c4", perms: 15 }
{ ename: "@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b", perms: 1 }
]
denials: { enames: [], conditions: [] }
default_perms: 0
require: []
}
}) {
metaEnvelope { id }
errors { message }
}
}
```

The owner gets `15` (`0x0F`, everything); one platform gets `1` (`0x01`, read). Nobody else is admitted: `require: []` means no group can pass, so step 3 always refuses.

Send `acl` as well. It is still required by the schema, and it is what any record without a policy falls back to — but where `_acl` is present it is ignored entirely, so its value does not matter.

Available on `createMetaEnvelope`, `storeMetaEnvelope`, `updateMetaEnvelope`, `updateMetaEnvelopeById`, `bulkCreateMetaEnvelopes`, and `uploadFile`.

## Permission values

| Want | `perms` | Hex |
|---|---|---|
| Read only | `1` | `0x01` |
| Read + add, but not edit | `3` | `0x03` |
| Read + edit | `5` | `0x05` |
| Everything | `15` | `0x0F` |

Bits: `1` READ, `2` CREATE, `4` UPDATE, `8` DELETE. Union them.

Two values to avoid sending by accident:

- **`0`** is not "no permissions", it is *no grant at all* — the party falls through to the ontology step as though you had never named them. To actually give someone nothing, leave them out and let the default refuse them.
- **Anything above `15`** is rejected outright. Bits 4–7 are reserved, and a write that sets one fails loudly rather than being quietly narrowed.

## Common shapes

**Owner-only.** Nothing but the owner, no fallback.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

**Public read, owner writes.** The empty group always passes, so anyone reaches `default_perms`.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

This is the closest equivalent of the legacy `["*"]`, except that everyone other than the owner is now read-only rather than able to write.

**Public read, one platform excluded.**

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": ["@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b"], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

A denial beats everything, including a grant to the same party. This is how a user shuts out a platform they do not trust without having to enumerate the ones they do.

**Append-only log.** A collaborator may add entries but never rewrite or remove one.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 },
{ "ename": "@collaborator", "perms": 3 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

## Acting on behalf of a user

Your platform's token proves your platform. It says nothing about which of your users a request is for, which matters as soon as a policy grants anything at user level.

Send the user's eName in `X-ON-BEHALF-OF`:

```http
POST /graphql
Authorization: Bearer <your platform token>
X-ENAME: @<vault owner>
X-ON-BEHALF-OF: @<the user you are acting for>
```

That user becomes the party the policy is evaluated against, and your platform is recorded alongside them — so a user grant applies at user specificity while a grant to your platform still applies at platform specificity. Omit the header and your platform is the party.

Two things to be clear about:

- **It is your assertion, not a proof.** The eVault has no way to check it, so it trusts you. That also means it will let you reach what the user was granted, which may be broader than your own grant. Do not send a user's eName on a request that user did not actually initiate.
- **It will not get you past a denial.** Denials match your platform as well as the asserted user, so a policy that excludes your platform still excludes it whatever name you send.

Only `@`-prefixed eNames count as parties. Anything else is ignored rather than treated as an identity.

## Reading a policy back

`_acl` is a field on `MetaEnvelope`:

```graphql
query {
metaEnvelope(id: "…") {
_acl {
grants { ename perms }
denials { enames }
default_perms
}
}
}
```

You always get the policy actually in force. A record written with only `acl: ["*"]` reports `default_perms: 15` behind an always-passing group rather than returning the array, so you can render one consistent view without caring how the record was written.

## Things that will bite you

**A grant is final.** If your platform is named in `grants`, that grant decides the answer on its own. It never falls through to `default_perms` — so a platform granted `1` on a record whose `default_perms` is `15` has read access, not full access. Being named is not always an upgrade.

**The most specific grant wins outright.** A grant to a user beats one to a platform, and they are not combined. If a record grants your platform `15` and the acting user `1`, a request carrying that user identity gets `1`. The platform's broader grant is not consulted.

**A valid platform token does not open a policied record.** It still works on records with no `_acl`. That bypass is exactly what a policy exists to close, so do not rely on your token to reach data a user has locked down — handle the refusal instead.

**Updates preserve the policy.** An `updateMetaEnvelope` that omits `_acl` leaves the stored policy alone rather than clearing it. To change a policy, send the new one in full — it replaces, it does not merge.

**A policy is visible to everyone who can read the record.** `_acl` is returned, not stripped — so your denial list tells any permitted reader which platforms the user excluded, and your grant list tells them who else has access. Do not put anything in a policy you would not show to its readers.

**Refusals look like two different things.** A record you may not touch raises `Access denied`. A record that does not exist for that eName returns `null`. Do not treat the second as the first — retrying will not help, and neither will asking for a different verb.

## The adapter does not do this yet

`EVaultClient` hardcodes `acl: ["*"]` and has no `_acl` parameter, so records written through `handleChange` cannot carry a policy today. To set one, call the eVault GraphQL endpoint directly for that record.

Everything else about your integration is unchanged — mapping, webhooks, and the Awareness Protocol do not interact with the policy. The policy is stored inside the record, so it travels with the data when it syncs, without your webhook controller doing anything.

## Errors you will get

A malformed policy is rejected whole — nothing is quietly dropped and stored in a weaker form. Every message is prefixed `Invalid _acl:` unless noted.

| Message | Cause |
|---|---|
| `bits 4-7 are reserved and must be 0` | A `perms` or `default_perms` above `15`. |
| `expected an unsigned byte` | `perms` was not an integer in 0-255. |
| `each grant needs an ename` | A grant object missing its `ename`. |
| `unknown operator "…"` | A condition `op` outside `>=`, `>`, `<=`, `<`, `==`. |
| `needs a finite numeric value` | A condition `value` that is not a number. |
| `grants must be an array` (and similar) | A container sent as the wrong shape. |
| `Unsupported _acl version: n` | `v` set to anything but `1`. |

Condition errors name the position — `require[0][1]`, `denials.conditions[0]` — so you can find the offending entry directly.

Two runtime outcomes worth distinguishing, neither of which is a validation error:

- **`Access denied`** — the record exists and the policy refused you. Retrying will not help; asking for a different verb might.
- **`null`** — no record with that id for that `X-ENAME`. Not a permissions problem.

List queries behave differently again: a record you may not read is **omitted from the results**, not reported. So a list can come back shorter than you expect with no error and no indication that anything was withheld. Do not treat a list's length as a count of what exists.

## Not usable yet

Two parts of the protocol document are specified but not connected, and a policy relying on them will not behave as written:

- **Groups.** Group membership is not resolved, so a grant or denial naming a group matches nobody. A group grant simply fails to apply; a **group denial silently fails to deny**, which is the dangerous direction. Name parties individually for now.
- **Ontology conditions.** No evaluator is wired in, so any condition fails. A `require` group containing conditions can never pass, and a deny condition always fires and refuses everyone. Until that lands, use only `grants`, `denials.enames`, and `require: []` or `require: [[]]`.

Enforcement is eVault-side. Platforms and the adapter do not evaluate policies themselves, so do not treat a policy as a reason to skip your own authorization checks.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the protocol model and wire format
- [eVault](/docs/Infrastructure/eVault) — where policies are stored and enforced
- [Webhook Controller](/docs/Post%20Platform%20Guide/webhook-controller) — the inbound side, unaffected by policies
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/ai-agent-skill.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---

# AI Agent Skill
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# eCurrency: Accounts and Ledger MetaEnvelopes
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
---

# Registering a Platform eVault
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
---

# PP Auth demonstrator
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
---

# Authenticating your platform
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,15 @@ The three gates run in order, and each can only narrow the one before it:

A grant cannot widen a certificate. Permitting `health:Read` to a platform never certified for `health` changes nothing.

## Where the record's own rules fit

The policy above is a signed statement about a *subject* — which platforms an owner will deal with at all. It is not stored in the data it protects.

[Access control](/docs/W3DS%20Protocol/Access-Control) is the other half: an `_acl` block inside each record, naming parties and the verbs they hold, plus ontology conditions admitting platforms that were never named. That block is what the eVault evaluates on each request, and it travels with the record when it syncs.

The two are separate gates and neither can widen the other.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the per-record `_acl` policy the eVault enforces
- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
22 changes: 20 additions & 2 deletions docs/docs/W3DS Basics/glossary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,13 @@ Definitions of key terms used across the W3DS and MetaState documentation. Where

## Access

The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by [ACLs](/docs/Infrastructure/eVault#access-control) and [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.
The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by the record's own access policy — [granular access control](/docs/W3DS%20Protocol/Access-Control) or the legacy [ACL](/docs/Infrastructure/eVault#access-control) array — and by [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.

---

## Access Control List (ACL)

The rules stored inside a record saying who may do what with it. Held in the record's `_acl` block as [grants](#grant), [denials](#denial), and ontology conditions, so the rules travel with the data when it syncs rather than living in a table beside it. The older `acl` string array is the same idea without per-verb granularity. See [Access Control](/docs/W3DS%20Protocol/Access-Control).

---

Expand DownExpand Up@@ -38,6 +44,12 @@ A set of data relating to an identifier that is signed by an issuing party (e.g.

---

## Denial

An entry in an [ACL](#access-control-list-acl) that removes access from a party, either by naming its [eName](#web-30-identifier-w3id--ename) or by stating a condition it must clear. A denial overrides any grant — it is the one place where a more specific rule does not win, because deny always does.

---

## eID (ePassport)

A document, similar to X.509, which binds a user's [W3ID](#web-30-identifier-w3id-ename) and the user's [Public Key](#public-key). It is signed by a [digital] notary participating in PKI. See [eID Wallet](/docs/Infrastructure/eID-Wallet) for how the prototype uses eID and key binding.
Expand All@@ -58,7 +70,7 @@ A non-human object within the MetaState such as an organization, a platform, a b

## Envelope

The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition and ACL that defines who is allowed to access it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.
The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition, and its MetaEnvelope carries the access policy that defines who is allowed to do what with it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.

---

Expand All@@ -68,6 +80,12 @@ A secure storage location or server for the management of data and credentials o

---

## Grant

An entry in an [ACL](#access-control-list-acl) pairing a party's [eName](#web-30-identifier-w3id--ename) with the permissions it holds, as a bitmask of Read, Create, Update and Delete. Where several grants could apply, only the most specific is used — a grant to a user beats one to a platform, which beats one to a group — and less specific grants do not add to it.

---

## Group

An [Entity](#entity): a reference to a number of users within the MetaState that holds its own [W3ID](#web-30-identifier-w3id-ename) and [eVault](#evault). It is often seen as a "group" in social networks.
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions docs/docs/Infrastructure/eVault.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,7 +65,8 @@ A **MetaEnvelope** is the top-level container for an entity (post, user, message

- **id**: Unique identifier (W3ID). Note: Only IDs registered in the Registry are guaranteed to be globally unique.
- **ontology**: Schema identifier (W3ID, e.g., "550e8400-e29b-41d4-a716-446655440001"). Schema W3IDs can be resolved to their schema definitions via the [Ontology](/docs/Infrastructure/Ontology) service. See [W3DS Basics](/docs/W3DS%20Basics/getting-started) for more information on ontology schemas.
- **acl**: Access Control List (who can access this data)
- **acl**: Legacy access control list (who can access this data)
- **_acl**: The granular access policy — grants, denials, and ontology conditions. Takes precedence over `acl` when present. See [Access Control](/docs/W3DS%20Protocol/Access-Control).
- **envelopes**: Array of individual Envelope nodes

### Envelopes
Expand All@@ -83,7 +84,7 @@ Each field in a MetaEnvelope becomes a separate **Envelope** node in Neo4j:
In Neo4j, the structure looks like:

```cypher
(MetaEnvelope {id, ontology, acl}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
(MetaEnvelope {id, ontology, acl, aclBlock}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
```

This flat graph structure allows:
Expand DownExpand Up@@ -603,26 +604,42 @@ curl -X GET "http://localhost:4000/logs?limit=20&cursor=2025-02-04T12:00:00.000Z

## Access Control

eVault uses **Access Control Lists (ACLs)** to determine who can access data.
A MetaEnvelope can carry access rules two ways. The granular `_acl` policy is the current model; the legacy `acl` array predates it and still works.

### ACL Format
### The `_acl` policy

ACLs are arrays of W3IDs or special values:
`_acl` holds grants (an eName plus a READ/CREATE/UPDATE/DELETE bitmask), denials, and Resource Link Ontology conditions. Decisions run in a fixed order — denials, then the most specific grant, then the ontology groups — and the most specific grant wins without unioning less specific ones.

Full model, wire format, and current limits: [Access Control](/docs/W3DS%20Protocol/Access-Control).

It is stored on the MetaEnvelope node as the `aclBlock` property (JSON), so the policy travels with the record when it syncs. No migration is needed to start using it: the property is optional, and a node without one is read through its legacy array exactly as before.

:::caution Rolling back

Once records begin carrying policies, treat the deployment as forward-only. Earlier builds do not read `aclBlock` and fall back to the `acl` array — which platforms write as `["*"]` — so a record an owner had locked down would become world-readable again on a rollback.

:::

### Legacy ACL format

Arrays of W3IDs or special values:

- `["*"]`: Public read access (anyone can read, but only the eVault owner can write)
- `["@user-a.w3id"]`: Only User A can access (read and write)
- `["@user-a.w3id", "@user-b.w3id"]`: User A and User B can access (read and write)

**Prototype Limitation**: In the current prototype implementation, ACLs provide all-or-nothing access. There is no read-only access withoutwrite access (except for `["*"]` which provides read-only access for everyone). More granular permissions are planned for future versions.
The array is all-or-nothing: there is no read-only-without-write except `["*"]`. That is what `_acl` replaces. A record with an `_acl` block ignores its array entirely; a record without one behaves exactly as it always has.

### Access Enforcement

The Access Guard middleware enforces ACLs:
The Access Guard middleware enforces access on every operation, with the permission the operation needs (read for queries, create/update/delete for the corresponding mutations):

1. **Extract W3ID**: From `X-ENAME` header or [Bearer token](/docs/W3DS%20Protocol/Authentication)
2. **Check ACL**: Verify the requesting W3ID is in the MetaEnvelope's ACL
3. **Filter Results**: Remove ACL field from responses (security)
4. **Allow/Deny**: Grant or deny access based on ACL
2. **Check the policy**: If the record carries `_acl`, decide by it. Otherwise fall back to the legacy array.
3. **Filter Results**: Remove the legacy `acl` array from responses; `_acl` is returned as the policy in force
4. **Allow/Deny**

A valid Registry-issued platform token satisfies the *legacy* path — but it does **not** bypass an `_acl` policy. A record carrying a policy is decided by that policy for every caller.

### Special Cases

Expand Down
209 changes: 209 additions & 0 deletions docs/docs/Post Platform Guide/access-control.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
---
sidebar_position: 6
---

# Implementing Access Control

Your platform writes records into a user's eVault. By default those records are wide open — anything that syncs to a platform can be read by it. This page is how you narrow that.

For the model itself — the bitmask, specificity, the decision order — see [Access Control](/docs/W3DS%20Protocol/Access-Control) in the protocol section. This page is the practical side: what to send, what comes back, and what will bite you.

## What you get if you do nothing

The Web3 Adapter writes every record with `acl: ["*"]`. That means anyone, everything, and it is what all existing platform data looks like today.

Nothing about that changes on its own. Records with no `_acl` block keep behaving exactly as they always have, including the part where any platform holding a valid Registry-issued token can reach them. You opt in per record by sending a policy.

## Setting a policy

`_acl` is an optional field on the same inputs you already use.

```graphql
mutation {
createMetaEnvelope(input: {
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: { content: "…", authorId: "…" }
acl: ["*"]
_acl: {
v: 1
grants: [
{ ename: "@7b9c2e1a-4f30-4c5e-9a21-d8e0f1a2b3c4", perms: 15 }
{ ename: "@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b", perms: 1 }
]
denials: { enames: [], conditions: [] }
default_perms: 0
require: []
}
}) {
metaEnvelope { id }
errors { message }
}
}
```

The owner gets `15` (`0x0F`, everything); one platform gets `1` (`0x01`, read). Nobody else is admitted: `require: []` means no group can pass, so step 3 always refuses.

Send `acl` as well. It is still required by the schema, and it is what any record without a policy falls back to — but where `_acl` is present it is ignored entirely, so its value does not matter.

Available on `createMetaEnvelope`, `storeMetaEnvelope`, `updateMetaEnvelope`, `updateMetaEnvelopeById`, `bulkCreateMetaEnvelopes`, and `uploadFile`.

## Permission values

| Want | `perms` | Hex |
|---|---|---|
| Read only | `1` | `0x01` |
| Read + add, but not edit | `3` | `0x03` |
| Read + edit | `5` | `0x05` |
| Everything | `15` | `0x0F` |

Bits: `1` READ, `2` CREATE, `4` UPDATE, `8` DELETE. Union them.

Two values to avoid sending by accident:

- **`0`** is not "no permissions", it is *no grant at all* — the party falls through to the ontology step as though you had never named them. To actually give someone nothing, leave them out and let the default refuse them.
- **Anything above `15`** is rejected outright. Bits 4–7 are reserved, and a write that sets one fails loudly rather than being quietly narrowed.

## Common shapes

**Owner-only.** Nothing but the owner, no fallback.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

**Public read, owner writes.** The empty group always passes, so anyone reaches `default_perms`.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

This is the closest equivalent of the legacy `["*"]`, except that everyone other than the owner is now read-only rather than able to write.

**Public read, one platform excluded.**

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": ["@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b"], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

A denial beats everything, including a grant to the same party. This is how a user shuts out a platform they do not trust without having to enumerate the ones they do.

**Append-only log.** A collaborator may add entries but never rewrite or remove one.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 },
{ "ename": "@collaborator", "perms": 3 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

## Acting on behalf of a user

Your platform's token proves your platform. It says nothing about which of your users a request is for, which matters as soon as a policy grants anything at user level.

Send the user's eName in `X-ON-BEHALF-OF`:

```http
POST /graphql
Authorization: Bearer <your platform token>
X-ENAME: @<vault owner>
X-ON-BEHALF-OF: @<the user you are acting for>
```

That user becomes the party the policy is evaluated against, and your platform is recorded alongside them — so a user grant applies at user specificity while a grant to your platform still applies at platform specificity. Omit the header and your platform is the party.

Two things to be clear about:

- **It is your assertion, not a proof.** The eVault has no way to check it, so it trusts you. That also means it will let you reach what the user was granted, which may be broader than your own grant. Do not send a user's eName on a request that user did not actually initiate.
- **It will not get you past a denial.** Denials match your platform as well as the asserted user, so a policy that excludes your platform still excludes it whatever name you send.

Only `@`-prefixed eNames count as parties. Anything else is ignored rather than treated as an identity.

## Reading a policy back

`_acl` is a field on `MetaEnvelope`:

```graphql
query {
metaEnvelope(id: "…") {
_acl {
grants { ename perms }
denials { enames }
default_perms
}
}
}
```

You always get the policy actually in force. A record written with only `acl: ["*"]` reports `default_perms: 15` behind an always-passing group rather than returning the array, so you can render one consistent view without caring how the record was written.

## Things that will bite you

**A grant is final.** If your platform is named in `grants`, that grant decides the answer on its own. It never falls through to `default_perms` — so a platform granted `1` on a record whose `default_perms` is `15` has read access, not full access. Being named is not always an upgrade.

**The most specific grant wins outright.** A grant to a user beats one to a platform, and they are not combined. If a record grants your platform `15` and the acting user `1`, a request carrying that user identity gets `1`. The platform's broader grant is not consulted.

**A valid platform token does not open a policied record.** It still works on records with no `_acl`. That bypass is exactly what a policy exists to close, so do not rely on your token to reach data a user has locked down — handle the refusal instead.

**Updates preserve the policy.** An `updateMetaEnvelope` that omits `_acl` leaves the stored policy alone rather than clearing it. To change a policy, send the new one in full — it replaces, it does not merge.

**A policy is visible to everyone who can read the record.** `_acl` is returned, not stripped — so your denial list tells any permitted reader which platforms the user excluded, and your grant list tells them who else has access. Do not put anything in a policy you would not show to its readers.

**Refusals look like two different things.** A record you may not touch raises `Access denied`. A record that does not exist for that eName returns `null`. Do not treat the second as the first — retrying will not help, and neither will asking for a different verb.

## The adapter does not do this yet

`EVaultClient` hardcodes `acl: ["*"]` and has no `_acl` parameter, so records written through `handleChange` cannot carry a policy today. To set one, call the eVault GraphQL endpoint directly for that record.

Everything else about your integration is unchanged — mapping, webhooks, and the Awareness Protocol do not interact with the policy. The policy is stored inside the record, so it travels with the data when it syncs, without your webhook controller doing anything.

## Errors you will get

A malformed policy is rejected whole — nothing is quietly dropped and stored in a weaker form. Every message is prefixed `Invalid _acl:` unless noted.

| Message | Cause |
|---|---|
| `bits 4-7 are reserved and must be 0` | A `perms` or `default_perms` above `15`. |
| `expected an unsigned byte` | `perms` was not an integer in 0-255. |
| `each grant needs an ename` | A grant object missing its `ename`. |
| `unknown operator "…"` | A condition `op` outside `>=`, `>`, `<=`, `<`, `==`. |
| `needs a finite numeric value` | A condition `value` that is not a number. |
| `grants must be an array` (and similar) | A container sent as the wrong shape. |
| `Unsupported _acl version: n` | `v` set to anything but `1`. |

Condition errors name the position — `require[0][1]`, `denials.conditions[0]` — so you can find the offending entry directly.

Two runtime outcomes worth distinguishing, neither of which is a validation error:

- **`Access denied`** — the record exists and the policy refused you. Retrying will not help; asking for a different verb might.
- **`null`** — no record with that id for that `X-ENAME`. Not a permissions problem.

List queries behave differently again: a record you may not read is **omitted from the results**, not reported. So a list can come back shorter than you expect with no error and no indication that anything was withheld. Do not treat a list's length as a count of what exists.

## Not usable yet

Two parts of the protocol document are specified but not connected, and a policy relying on them will not behave as written:

- **Groups.** Group membership is not resolved, so a grant or denial naming a group matches nobody. A group grant simply fails to apply; a **group denial silently fails to deny**, which is the dangerous direction. Name parties individually for now.
- **Ontology conditions.** No evaluator is wired in, so any condition fails. A `require` group containing conditions can never pass, and a deny condition always fires and refuses everyone. Until that lands, use only `grants`, `denials.enames`, and `require: []` or `require: [[]]`.

Enforcement is eVault-side. Platforms and the adapter do not evaluate policies themselves, so do not treat a policy as a reason to skip your own authorization checks.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the protocol model and wire format
- [eVault](/docs/Infrastructure/eVault) — where policies are stored and enforced
- [Webhook Controller](/docs/Post%20Platform%20Guide/webhook-controller) — the inbound side, unaffected by policies
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/ai-agent-skill.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---

# AI Agent Skill
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# eCurrency: Accounts and Ledger MetaEnvelopes
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
---

# Registering a Platform eVault
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
---

# PP Auth demonstrator
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
---

# Authenticating your platform
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,15 @@ The three gates run in order, and each can only narrow the one before it:

A grant cannot widen a certificate. Permitting `health:Read` to a platform never certified for `health` changes nothing.

## Where the record's own rules fit

The policy above is a signed statement about a *subject* — which platforms an owner will deal with at all. It is not stored in the data it protects.

[Access control](/docs/W3DS%20Protocol/Access-Control) is the other half: an `_acl` block inside each record, naming parties and the verbs they hold, plus ontology conditions admitting platforms that were never named. That block is what the eVault evaluates on each request, and it travels with the record when it syncs.

The two are separate gates and neither can widen the other.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the per-record `_acl` policy the eVault enforces
- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
22 changes: 20 additions & 2 deletions docs/docs/W3DS Basics/glossary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,13 @@ Definitions of key terms used across the W3DS and MetaState documentation. Where

## Access

The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by [ACLs](/docs/Infrastructure/eVault#access-control) and [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.
The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by the record's own access policy — [granular access control](/docs/W3DS%20Protocol/Access-Control) or the legacy [ACL](/docs/Infrastructure/eVault#access-control) array — and by [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.

---

## Access Control List (ACL)

The rules stored inside a record saying who may do what with it. Held in the record's `_acl` block as [grants](#grant), [denials](#denial), and ontology conditions, so the rules travel with the data when it syncs rather than living in a table beside it. The older `acl` string array is the same idea without per-verb granularity. See [Access Control](/docs/W3DS%20Protocol/Access-Control).

---

Expand DownExpand Up@@ -38,6 +44,12 @@ A set of data relating to an identifier that is signed by an issuing party (e.g.

---

## Denial

An entry in an [ACL](#access-control-list-acl) that removes access from a party, either by naming its [eName](#web-30-identifier-w3id--ename) or by stating a condition it must clear. A denial overrides any grant — it is the one place where a more specific rule does not win, because deny always does.

---

## eID (ePassport)

A document, similar to X.509, which binds a user's [W3ID](#web-30-identifier-w3id-ename) and the user's [Public Key](#public-key). It is signed by a [digital] notary participating in PKI. See [eID Wallet](/docs/Infrastructure/eID-Wallet) for how the prototype uses eID and key binding.
Expand All@@ -58,7 +70,7 @@ A non-human object within the MetaState such as an organization, a platform, a b

## Envelope

The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition and ACL that defines who is allowed to access it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.
The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition, and its MetaEnvelope carries the access policy that defines who is allowed to do what with it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.

---

Expand All@@ -68,6 +80,12 @@ A secure storage location or server for the management of data and credentials o

---

## Grant

An entry in an [ACL](#access-control-list-acl) pairing a party's [eName](#web-30-identifier-w3id--ename) with the permissions it holds, as a bitmask of Read, Create, Update and Delete. Where several grants could apply, only the most specific is used — a grant to a user beats one to a platform, which beats one to a group — and less specific grants do not add to it.

---

## Group

An [Entity](#entity): a reference to a number of users within the MetaState that holds its own [W3ID](#web-30-identifier-w3id-ename) and [eVault](#evault). It is often seen as a "group" in social networks.
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions docs/docs/Infrastructure/eVault.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,7 +65,8 @@ A **MetaEnvelope** is the top-level container for an entity (post, user, message

- **id**: Unique identifier (W3ID). Note: Only IDs registered in the Registry are guaranteed to be globally unique.
- **ontology**: Schema identifier (W3ID, e.g., "550e8400-e29b-41d4-a716-446655440001"). Schema W3IDs can be resolved to their schema definitions via the [Ontology](/docs/Infrastructure/Ontology) service. See [W3DS Basics](/docs/W3DS%20Basics/getting-started) for more information on ontology schemas.
- **acl**: Access Control List (who can access this data)
- **acl**: Legacy access control list (who can access this data)
- **_acl**: The granular access policy — grants, denials, and ontology conditions. Takes precedence over `acl` when present. See [Access Control](/docs/W3DS%20Protocol/Access-Control).
- **envelopes**: Array of individual Envelope nodes

### Envelopes
Expand All@@ -83,7 +84,7 @@ Each field in a MetaEnvelope becomes a separate **Envelope** node in Neo4j:
In Neo4j, the structure looks like:

```cypher
(MetaEnvelope {id, ontology, acl}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
(MetaEnvelope {id, ontology, acl, aclBlock}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
```

This flat graph structure allows:
Expand DownExpand Up@@ -603,26 +604,42 @@ curl -X GET "http://localhost:4000/logs?limit=20&cursor=2025-02-04T12:00:00.000Z

## Access Control

eVault uses **Access Control Lists (ACLs)** to determine who can access data.
A MetaEnvelope can carry access rules two ways. The granular `_acl` policy is the current model; the legacy `acl` array predates it and still works.

### ACL Format
### The `_acl` policy

ACLs are arrays of W3IDs or special values:
`_acl` holds grants (an eName plus a READ/CREATE/UPDATE/DELETE bitmask), denials, and Resource Link Ontology conditions. Decisions run in a fixed order — denials, then the most specific grant, then the ontology groups — and the most specific grant wins without unioning less specific ones.

Full model, wire format, and current limits: [Access Control](/docs/W3DS%20Protocol/Access-Control).

It is stored on the MetaEnvelope node as the `aclBlock` property (JSON), so the policy travels with the record when it syncs. No migration is needed to start using it: the property is optional, and a node without one is read through its legacy array exactly as before.

:::caution Rolling back

Once records begin carrying policies, treat the deployment as forward-only. Earlier builds do not read `aclBlock` and fall back to the `acl` array — which platforms write as `["*"]` — so a record an owner had locked down would become world-readable again on a rollback.

:::

### Legacy ACL format

Arrays of W3IDs or special values:

- `["*"]`: Public read access (anyone can read, but only the eVault owner can write)
- `["@user-a.w3id"]`: Only User A can access (read and write)
- `["@user-a.w3id", "@user-b.w3id"]`: User A and User B can access (read and write)

**Prototype Limitation**: In the current prototype implementation, ACLs provide all-or-nothing access. There is no read-only access withoutwrite access (except for `["*"]` which provides read-only access for everyone). More granular permissions are planned for future versions.
The array is all-or-nothing: there is no read-only-without-write except `["*"]`. That is what `_acl` replaces. A record with an `_acl` block ignores its array entirely; a record without one behaves exactly as it always has.

### Access Enforcement

The Access Guard middleware enforces ACLs:
The Access Guard middleware enforces access on every operation, with the permission the operation needs (read for queries, create/update/delete for the corresponding mutations):

1. **Extract W3ID**: From `X-ENAME` header or [Bearer token](/docs/W3DS%20Protocol/Authentication)
2. **Check ACL**: Verify the requesting W3ID is in the MetaEnvelope's ACL
3. **Filter Results**: Remove ACL field from responses (security)
4. **Allow/Deny**: Grant or deny access based on ACL
2. **Check the policy**: If the record carries `_acl`, decide by it. Otherwise fall back to the legacy array.
3. **Filter Results**: Remove the legacy `acl` array from responses; `_acl` is returned as the policy in force
4. **Allow/Deny**

A valid Registry-issued platform token satisfies the *legacy* path — but it does **not** bypass an `_acl` policy. A record carrying a policy is decided by that policy for every caller.

### Special Cases

Expand Down
209 changes: 209 additions & 0 deletions docs/docs/Post Platform Guide/access-control.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
---
sidebar_position: 6
---

# Implementing Access Control

Your platform writes records into a user's eVault. By default those records are wide open — anything that syncs to a platform can be read by it. This page is how you narrow that.

For the model itself — the bitmask, specificity, the decision order — see [Access Control](/docs/W3DS%20Protocol/Access-Control) in the protocol section. This page is the practical side: what to send, what comes back, and what will bite you.

## What you get if you do nothing

The Web3 Adapter writes every record with `acl: ["*"]`. That means anyone, everything, and it is what all existing platform data looks like today.

Nothing about that changes on its own. Records with no `_acl` block keep behaving exactly as they always have, including the part where any platform holding a valid Registry-issued token can reach them. You opt in per record by sending a policy.

## Setting a policy

`_acl` is an optional field on the same inputs you already use.

```graphql
mutation {
createMetaEnvelope(input: {
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: { content: "…", authorId: "…" }
acl: ["*"]
_acl: {
v: 1
grants: [
{ ename: "@7b9c2e1a-4f30-4c5e-9a21-d8e0f1a2b3c4", perms: 15 }
{ ename: "@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b", perms: 1 }
]
denials: { enames: [], conditions: [] }
default_perms: 0
require: []
}
}) {
metaEnvelope { id }
errors { message }
}
}
```

The owner gets `15` (`0x0F`, everything); one platform gets `1` (`0x01`, read). Nobody else is admitted: `require: []` means no group can pass, so step 3 always refuses.

Send `acl` as well. It is still required by the schema, and it is what any record without a policy falls back to — but where `_acl` is present it is ignored entirely, so its value does not matter.

Available on `createMetaEnvelope`, `storeMetaEnvelope`, `updateMetaEnvelope`, `updateMetaEnvelopeById`, `bulkCreateMetaEnvelopes`, and `uploadFile`.

## Permission values

| Want | `perms` | Hex |
|---|---|---|
| Read only | `1` | `0x01` |
| Read + add, but not edit | `3` | `0x03` |
| Read + edit | `5` | `0x05` |
| Everything | `15` | `0x0F` |

Bits: `1` READ, `2` CREATE, `4` UPDATE, `8` DELETE. Union them.

Two values to avoid sending by accident:

- **`0`** is not "no permissions", it is *no grant at all* — the party falls through to the ontology step as though you had never named them. To actually give someone nothing, leave them out and let the default refuse them.
- **Anything above `15`** is rejected outright. Bits 4–7 are reserved, and a write that sets one fails loudly rather than being quietly narrowed.

## Common shapes

**Owner-only.** Nothing but the owner, no fallback.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

**Public read, owner writes.** The empty group always passes, so anyone reaches `default_perms`.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

This is the closest equivalent of the legacy `["*"]`, except that everyone other than the owner is now read-only rather than able to write.

**Public read, one platform excluded.**

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": ["@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b"], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

A denial beats everything, including a grant to the same party. This is how a user shuts out a platform they do not trust without having to enumerate the ones they do.

**Append-only log.** A collaborator may add entries but never rewrite or remove one.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 },
{ "ename": "@collaborator", "perms": 3 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

## Acting on behalf of a user

Your platform's token proves your platform. It says nothing about which of your users a request is for, which matters as soon as a policy grants anything at user level.

Send the user's eName in `X-ON-BEHALF-OF`:

```http
POST /graphql
Authorization: Bearer <your platform token>
X-ENAME: @<vault owner>
X-ON-BEHALF-OF: @<the user you are acting for>
```

That user becomes the party the policy is evaluated against, and your platform is recorded alongside them — so a user grant applies at user specificity while a grant to your platform still applies at platform specificity. Omit the header and your platform is the party.

Two things to be clear about:

- **It is your assertion, not a proof.** The eVault has no way to check it, so it trusts you. That also means it will let you reach what the user was granted, which may be broader than your own grant. Do not send a user's eName on a request that user did not actually initiate.
- **It will not get you past a denial.** Denials match your platform as well as the asserted user, so a policy that excludes your platform still excludes it whatever name you send.

Only `@`-prefixed eNames count as parties. Anything else is ignored rather than treated as an identity.

## Reading a policy back

`_acl` is a field on `MetaEnvelope`:

```graphql
query {
metaEnvelope(id: "…") {
_acl {
grants { ename perms }
denials { enames }
default_perms
}
}
}
```

You always get the policy actually in force. A record written with only `acl: ["*"]` reports `default_perms: 15` behind an always-passing group rather than returning the array, so you can render one consistent view without caring how the record was written.

## Things that will bite you

**A grant is final.** If your platform is named in `grants`, that grant decides the answer on its own. It never falls through to `default_perms` — so a platform granted `1` on a record whose `default_perms` is `15` has read access, not full access. Being named is not always an upgrade.

**The most specific grant wins outright.** A grant to a user beats one to a platform, and they are not combined. If a record grants your platform `15` and the acting user `1`, a request carrying that user identity gets `1`. The platform's broader grant is not consulted.

**A valid platform token does not open a policied record.** It still works on records with no `_acl`. That bypass is exactly what a policy exists to close, so do not rely on your token to reach data a user has locked down — handle the refusal instead.

**Updates preserve the policy.** An `updateMetaEnvelope` that omits `_acl` leaves the stored policy alone rather than clearing it. To change a policy, send the new one in full — it replaces, it does not merge.

**A policy is visible to everyone who can read the record.** `_acl` is returned, not stripped — so your denial list tells any permitted reader which platforms the user excluded, and your grant list tells them who else has access. Do not put anything in a policy you would not show to its readers.

**Refusals look like two different things.** A record you may not touch raises `Access denied`. A record that does not exist for that eName returns `null`. Do not treat the second as the first — retrying will not help, and neither will asking for a different verb.

## The adapter does not do this yet

`EVaultClient` hardcodes `acl: ["*"]` and has no `_acl` parameter, so records written through `handleChange` cannot carry a policy today. To set one, call the eVault GraphQL endpoint directly for that record.

Everything else about your integration is unchanged — mapping, webhooks, and the Awareness Protocol do not interact with the policy. The policy is stored inside the record, so it travels with the data when it syncs, without your webhook controller doing anything.

## Errors you will get

A malformed policy is rejected whole — nothing is quietly dropped and stored in a weaker form. Every message is prefixed `Invalid _acl:` unless noted.

| Message | Cause |
|---|---|
| `bits 4-7 are reserved and must be 0` | A `perms` or `default_perms` above `15`. |
| `expected an unsigned byte` | `perms` was not an integer in 0-255. |
| `each grant needs an ename` | A grant object missing its `ename`. |
| `unknown operator "…"` | A condition `op` outside `>=`, `>`, `<=`, `<`, `==`. |
| `needs a finite numeric value` | A condition `value` that is not a number. |
| `grants must be an array` (and similar) | A container sent as the wrong shape. |
| `Unsupported _acl version: n` | `v` set to anything but `1`. |

Condition errors name the position — `require[0][1]`, `denials.conditions[0]` — so you can find the offending entry directly.

Two runtime outcomes worth distinguishing, neither of which is a validation error:

- **`Access denied`** — the record exists and the policy refused you. Retrying will not help; asking for a different verb might.
- **`null`** — no record with that id for that `X-ENAME`. Not a permissions problem.

List queries behave differently again: a record you may not read is **omitted from the results**, not reported. So a list can come back shorter than you expect with no error and no indication that anything was withheld. Do not treat a list's length as a count of what exists.

## Not usable yet

Two parts of the protocol document are specified but not connected, and a policy relying on them will not behave as written:

- **Groups.** Group membership is not resolved, so a grant or denial naming a group matches nobody. A group grant simply fails to apply; a **group denial silently fails to deny**, which is the dangerous direction. Name parties individually for now.
- **Ontology conditions.** No evaluator is wired in, so any condition fails. A `require` group containing conditions can never pass, and a deny condition always fires and refuses everyone. Until that lands, use only `grants`, `denials.enames`, and `require: []` or `require: [[]]`.

Enforcement is eVault-side. Platforms and the adapter do not evaluate policies themselves, so do not treat a policy as a reason to skip your own authorization checks.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the protocol model and wire format
- [eVault](/docs/Infrastructure/eVault) — where policies are stored and enforced
- [Webhook Controller](/docs/Post%20Platform%20Guide/webhook-controller) — the inbound side, unaffected by policies
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/ai-agent-skill.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---

# AI Agent Skill
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# eCurrency: Accounts and Ledger MetaEnvelopes
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
---

# Registering a Platform eVault
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
---

# PP Auth demonstrator
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
---

# Authenticating your platform
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,15 @@ The three gates run in order, and each can only narrow the one before it:

A grant cannot widen a certificate. Permitting `health:Read` to a platform never certified for `health` changes nothing.

## Where the record's own rules fit

The policy above is a signed statement about a *subject* — which platforms an owner will deal with at all. It is not stored in the data it protects.

[Access control](/docs/W3DS%20Protocol/Access-Control) is the other half: an `_acl` block inside each record, naming parties and the verbs they hold, plus ontology conditions admitting platforms that were never named. That block is what the eVault evaluates on each request, and it travels with the record when it syncs.

The two are separate gates and neither can widen the other.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the per-record `_acl` policy the eVault enforces
- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
22 changes: 20 additions & 2 deletions docs/docs/W3DS Basics/glossary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,13 @@ Definitions of key terms used across the W3DS and MetaState documentation. Where

## Access

The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by [ACLs](/docs/Infrastructure/eVault#access-control) and [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.
The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by the record's own access policy — [granular access control](/docs/W3DS%20Protocol/Access-Control) or the legacy [ACL](/docs/Infrastructure/eVault#access-control) array — and by [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.

---

## Access Control List (ACL)

The rules stored inside a record saying who may do what with it. Held in the record's `_acl` block as [grants](#grant), [denials](#denial), and ontology conditions, so the rules travel with the data when it syncs rather than living in a table beside it. The older `acl` string array is the same idea without per-verb granularity. See [Access Control](/docs/W3DS%20Protocol/Access-Control).

---

Expand DownExpand Up@@ -38,6 +44,12 @@ A set of data relating to an identifier that is signed by an issuing party (e.g.

---

## Denial

An entry in an [ACL](#access-control-list-acl) that removes access from a party, either by naming its [eName](#web-30-identifier-w3id--ename) or by stating a condition it must clear. A denial overrides any grant — it is the one place where a more specific rule does not win, because deny always does.

---

## eID (ePassport)

A document, similar to X.509, which binds a user's [W3ID](#web-30-identifier-w3id-ename) and the user's [Public Key](#public-key). It is signed by a [digital] notary participating in PKI. See [eID Wallet](/docs/Infrastructure/eID-Wallet) for how the prototype uses eID and key binding.
Expand All@@ -58,7 +70,7 @@ A non-human object within the MetaState such as an organization, a platform, a b

## Envelope

The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition and ACL that defines who is allowed to access it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.
The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition, and its MetaEnvelope carries the access policy that defines who is allowed to do what with it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.

---

Expand All@@ -68,6 +80,12 @@ A secure storage location or server for the management of data and credentials o

---

## Grant

An entry in an [ACL](#access-control-list-acl) pairing a party's [eName](#web-30-identifier-w3id--ename) with the permissions it holds, as a bitmask of Read, Create, Update and Delete. Where several grants could apply, only the most specific is used — a grant to a user beats one to a platform, which beats one to a group — and less specific grants do not add to it.

---

## Group

An [Entity](#entity): a reference to a number of users within the MetaState that holds its own [W3ID](#web-30-identifier-w3id-ename) and [eVault](#evault). It is often seen as a "group" in social networks.
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions docs/docs/Infrastructure/eVault.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,7 +65,8 @@ A **MetaEnvelope** is the top-level container for an entity (post, user, message

- **id**: Unique identifier (W3ID). Note: Only IDs registered in the Registry are guaranteed to be globally unique.
- **ontology**: Schema identifier (W3ID, e.g., "550e8400-e29b-41d4-a716-446655440001"). Schema W3IDs can be resolved to their schema definitions via the [Ontology](/docs/Infrastructure/Ontology) service. See [W3DS Basics](/docs/W3DS%20Basics/getting-started) for more information on ontology schemas.
- **acl**: Access Control List (who can access this data)
- **acl**: Legacy access control list (who can access this data)
- **_acl**: The granular access policy — grants, denials, and ontology conditions. Takes precedence over `acl` when present. See [Access Control](/docs/W3DS%20Protocol/Access-Control).
- **envelopes**: Array of individual Envelope nodes

### Envelopes
Expand All@@ -83,7 +84,7 @@ Each field in a MetaEnvelope becomes a separate **Envelope** node in Neo4j:
In Neo4j, the structure looks like:

```cypher
(MetaEnvelope {id, ontology, acl}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
(MetaEnvelope {id, ontology, acl, aclBlock}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
```

This flat graph structure allows:
Expand DownExpand Up@@ -603,26 +604,42 @@ curl -X GET "http://localhost:4000/logs?limit=20&cursor=2025-02-04T12:00:00.000Z

## Access Control

eVault uses **Access Control Lists (ACLs)** to determine who can access data.
A MetaEnvelope can carry access rules two ways. The granular `_acl` policy is the current model; the legacy `acl` array predates it and still works.

### ACL Format
### The `_acl` policy

ACLs are arrays of W3IDs or special values:
`_acl` holds grants (an eName plus a READ/CREATE/UPDATE/DELETE bitmask), denials, and Resource Link Ontology conditions. Decisions run in a fixed order — denials, then the most specific grant, then the ontology groups — and the most specific grant wins without unioning less specific ones.

Full model, wire format, and current limits: [Access Control](/docs/W3DS%20Protocol/Access-Control).

It is stored on the MetaEnvelope node as the `aclBlock` property (JSON), so the policy travels with the record when it syncs. No migration is needed to start using it: the property is optional, and a node without one is read through its legacy array exactly as before.

:::caution Rolling back

Once records begin carrying policies, treat the deployment as forward-only. Earlier builds do not read `aclBlock` and fall back to the `acl` array — which platforms write as `["*"]` — so a record an owner had locked down would become world-readable again on a rollback.

:::

### Legacy ACL format

Arrays of W3IDs or special values:

- `["*"]`: Public read access (anyone can read, but only the eVault owner can write)
- `["@user-a.w3id"]`: Only User A can access (read and write)
- `["@user-a.w3id", "@user-b.w3id"]`: User A and User B can access (read and write)

**Prototype Limitation**: In the current prototype implementation, ACLs provide all-or-nothing access. There is no read-only access withoutwrite access (except for `["*"]` which provides read-only access for everyone). More granular permissions are planned for future versions.
The array is all-or-nothing: there is no read-only-without-write except `["*"]`. That is what `_acl` replaces. A record with an `_acl` block ignores its array entirely; a record without one behaves exactly as it always has.

### Access Enforcement

The Access Guard middleware enforces ACLs:
The Access Guard middleware enforces access on every operation, with the permission the operation needs (read for queries, create/update/delete for the corresponding mutations):

1. **Extract W3ID**: From `X-ENAME` header or [Bearer token](/docs/W3DS%20Protocol/Authentication)
2. **Check ACL**: Verify the requesting W3ID is in the MetaEnvelope's ACL
3. **Filter Results**: Remove ACL field from responses (security)
4. **Allow/Deny**: Grant or deny access based on ACL
2. **Check the policy**: If the record carries `_acl`, decide by it. Otherwise fall back to the legacy array.
3. **Filter Results**: Remove the legacy `acl` array from responses; `_acl` is returned as the policy in force
4. **Allow/Deny**

A valid Registry-issued platform token satisfies the *legacy* path — but it does **not** bypass an `_acl` policy. A record carrying a policy is decided by that policy for every caller.

### Special Cases

Expand Down
209 changes: 209 additions & 0 deletions docs/docs/Post Platform Guide/access-control.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
---
sidebar_position: 6
---

# Implementing Access Control

Your platform writes records into a user's eVault. By default those records are wide open — anything that syncs to a platform can be read by it. This page is how you narrow that.

For the model itself — the bitmask, specificity, the decision order — see [Access Control](/docs/W3DS%20Protocol/Access-Control) in the protocol section. This page is the practical side: what to send, what comes back, and what will bite you.

## What you get if you do nothing

The Web3 Adapter writes every record with `acl: ["*"]`. That means anyone, everything, and it is what all existing platform data looks like today.

Nothing about that changes on its own. Records with no `_acl` block keep behaving exactly as they always have, including the part where any platform holding a valid Registry-issued token can reach them. You opt in per record by sending a policy.

## Setting a policy

`_acl` is an optional field on the same inputs you already use.

```graphql
mutation {
createMetaEnvelope(input: {
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: { content: "…", authorId: "…" }
acl: ["*"]
_acl: {
v: 1
grants: [
{ ename: "@7b9c2e1a-4f30-4c5e-9a21-d8e0f1a2b3c4", perms: 15 }
{ ename: "@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b", perms: 1 }
]
denials: { enames: [], conditions: [] }
default_perms: 0
require: []
}
}) {
metaEnvelope { id }
errors { message }
}
}
```

The owner gets `15` (`0x0F`, everything); one platform gets `1` (`0x01`, read). Nobody else is admitted: `require: []` means no group can pass, so step 3 always refuses.

Send `acl` as well. It is still required by the schema, and it is what any record without a policy falls back to — but where `_acl` is present it is ignored entirely, so its value does not matter.

Available on `createMetaEnvelope`, `storeMetaEnvelope`, `updateMetaEnvelope`, `updateMetaEnvelopeById`, `bulkCreateMetaEnvelopes`, and `uploadFile`.

## Permission values

| Want | `perms` | Hex |
|---|---|---|
| Read only | `1` | `0x01` |
| Read + add, but not edit | `3` | `0x03` |
| Read + edit | `5` | `0x05` |
| Everything | `15` | `0x0F` |

Bits: `1` READ, `2` CREATE, `4` UPDATE, `8` DELETE. Union them.

Two values to avoid sending by accident:

- **`0`** is not "no permissions", it is *no grant at all* — the party falls through to the ontology step as though you had never named them. To actually give someone nothing, leave them out and let the default refuse them.
- **Anything above `15`** is rejected outright. Bits 4–7 are reserved, and a write that sets one fails loudly rather than being quietly narrowed.

## Common shapes

**Owner-only.** Nothing but the owner, no fallback.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

**Public read, owner writes.** The empty group always passes, so anyone reaches `default_perms`.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

This is the closest equivalent of the legacy `["*"]`, except that everyone other than the owner is now read-only rather than able to write.

**Public read, one platform excluded.**

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": ["@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b"], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

A denial beats everything, including a grant to the same party. This is how a user shuts out a platform they do not trust without having to enumerate the ones they do.

**Append-only log.** A collaborator may add entries but never rewrite or remove one.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 },
{ "ename": "@collaborator", "perms": 3 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

## Acting on behalf of a user

Your platform's token proves your platform. It says nothing about which of your users a request is for, which matters as soon as a policy grants anything at user level.

Send the user's eName in `X-ON-BEHALF-OF`:

```http
POST /graphql
Authorization: Bearer <your platform token>
X-ENAME: @<vault owner>
X-ON-BEHALF-OF: @<the user you are acting for>
```

That user becomes the party the policy is evaluated against, and your platform is recorded alongside them — so a user grant applies at user specificity while a grant to your platform still applies at platform specificity. Omit the header and your platform is the party.

Two things to be clear about:

- **It is your assertion, not a proof.** The eVault has no way to check it, so it trusts you. That also means it will let you reach what the user was granted, which may be broader than your own grant. Do not send a user's eName on a request that user did not actually initiate.
- **It will not get you past a denial.** Denials match your platform as well as the asserted user, so a policy that excludes your platform still excludes it whatever name you send.

Only `@`-prefixed eNames count as parties. Anything else is ignored rather than treated as an identity.

## Reading a policy back

`_acl` is a field on `MetaEnvelope`:

```graphql
query {
metaEnvelope(id: "…") {
_acl {
grants { ename perms }
denials { enames }
default_perms
}
}
}
```

You always get the policy actually in force. A record written with only `acl: ["*"]` reports `default_perms: 15` behind an always-passing group rather than returning the array, so you can render one consistent view without caring how the record was written.

## Things that will bite you

**A grant is final.** If your platform is named in `grants`, that grant decides the answer on its own. It never falls through to `default_perms` — so a platform granted `1` on a record whose `default_perms` is `15` has read access, not full access. Being named is not always an upgrade.

**The most specific grant wins outright.** A grant to a user beats one to a platform, and they are not combined. If a record grants your platform `15` and the acting user `1`, a request carrying that user identity gets `1`. The platform's broader grant is not consulted.

**A valid platform token does not open a policied record.** It still works on records with no `_acl`. That bypass is exactly what a policy exists to close, so do not rely on your token to reach data a user has locked down — handle the refusal instead.

**Updates preserve the policy.** An `updateMetaEnvelope` that omits `_acl` leaves the stored policy alone rather than clearing it. To change a policy, send the new one in full — it replaces, it does not merge.

**A policy is visible to everyone who can read the record.** `_acl` is returned, not stripped — so your denial list tells any permitted reader which platforms the user excluded, and your grant list tells them who else has access. Do not put anything in a policy you would not show to its readers.

**Refusals look like two different things.** A record you may not touch raises `Access denied`. A record that does not exist for that eName returns `null`. Do not treat the second as the first — retrying will not help, and neither will asking for a different verb.

## The adapter does not do this yet

`EVaultClient` hardcodes `acl: ["*"]` and has no `_acl` parameter, so records written through `handleChange` cannot carry a policy today. To set one, call the eVault GraphQL endpoint directly for that record.

Everything else about your integration is unchanged — mapping, webhooks, and the Awareness Protocol do not interact with the policy. The policy is stored inside the record, so it travels with the data when it syncs, without your webhook controller doing anything.

## Errors you will get

A malformed policy is rejected whole — nothing is quietly dropped and stored in a weaker form. Every message is prefixed `Invalid _acl:` unless noted.

| Message | Cause |
|---|---|
| `bits 4-7 are reserved and must be 0` | A `perms` or `default_perms` above `15`. |
| `expected an unsigned byte` | `perms` was not an integer in 0-255. |
| `each grant needs an ename` | A grant object missing its `ename`. |
| `unknown operator "…"` | A condition `op` outside `>=`, `>`, `<=`, `<`, `==`. |
| `needs a finite numeric value` | A condition `value` that is not a number. |
| `grants must be an array` (and similar) | A container sent as the wrong shape. |
| `Unsupported _acl version: n` | `v` set to anything but `1`. |

Condition errors name the position — `require[0][1]`, `denials.conditions[0]` — so you can find the offending entry directly.

Two runtime outcomes worth distinguishing, neither of which is a validation error:

- **`Access denied`** — the record exists and the policy refused you. Retrying will not help; asking for a different verb might.
- **`null`** — no record with that id for that `X-ENAME`. Not a permissions problem.

List queries behave differently again: a record you may not read is **omitted from the results**, not reported. So a list can come back shorter than you expect with no error and no indication that anything was withheld. Do not treat a list's length as a count of what exists.

## Not usable yet

Two parts of the protocol document are specified but not connected, and a policy relying on them will not behave as written:

- **Groups.** Group membership is not resolved, so a grant or denial naming a group matches nobody. A group grant simply fails to apply; a **group denial silently fails to deny**, which is the dangerous direction. Name parties individually for now.
- **Ontology conditions.** No evaluator is wired in, so any condition fails. A `require` group containing conditions can never pass, and a deny condition always fires and refuses everyone. Until that lands, use only `grants`, `denials.enames`, and `require: []` or `require: [[]]`.

Enforcement is eVault-side. Platforms and the adapter do not evaluate policies themselves, so do not treat a policy as a reason to skip your own authorization checks.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the protocol model and wire format
- [eVault](/docs/Infrastructure/eVault) — where policies are stored and enforced
- [Webhook Controller](/docs/Post%20Platform%20Guide/webhook-controller) — the inbound side, unaffected by policies
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/ai-agent-skill.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---

# AI Agent Skill
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# eCurrency: Accounts and Ledger MetaEnvelopes
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
---

# Registering a Platform eVault
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
---

# PP Auth demonstrator
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
---

# Authenticating your platform
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,15 @@ The three gates run in order, and each can only narrow the one before it:

A grant cannot widen a certificate. Permitting `health:Read` to a platform never certified for `health` changes nothing.

## Where the record's own rules fit

The policy above is a signed statement about a *subject* — which platforms an owner will deal with at all. It is not stored in the data it protects.

[Access control](/docs/W3DS%20Protocol/Access-Control) is the other half: an `_acl` block inside each record, naming parties and the verbs they hold, plus ontology conditions admitting platforms that were never named. That block is what the eVault evaluates on each request, and it travels with the record when it syncs.

The two are separate gates and neither can widen the other.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the per-record `_acl` policy the eVault enforces
- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
22 changes: 20 additions & 2 deletions docs/docs/W3DS Basics/glossary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,13 @@ Definitions of key terms used across the W3DS and MetaState documentation. Where

## Access

The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by [ACLs](/docs/Infrastructure/eVault#access-control) and [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.
The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by the record's own access policy — [granular access control](/docs/W3DS%20Protocol/Access-Control) or the legacy [ACL](/docs/Infrastructure/eVault#access-control) array — and by [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.

---

## Access Control List (ACL)

The rules stored inside a record saying who may do what with it. Held in the record's `_acl` block as [grants](#grant), [denials](#denial), and ontology conditions, so the rules travel with the data when it syncs rather than living in a table beside it. The older `acl` string array is the same idea without per-verb granularity. See [Access Control](/docs/W3DS%20Protocol/Access-Control).

---

Expand DownExpand Up@@ -38,6 +44,12 @@ A set of data relating to an identifier that is signed by an issuing party (e.g.

---

## Denial

An entry in an [ACL](#access-control-list-acl) that removes access from a party, either by naming its [eName](#web-30-identifier-w3id--ename) or by stating a condition it must clear. A denial overrides any grant — it is the one place where a more specific rule does not win, because deny always does.

---

## eID (ePassport)

A document, similar to X.509, which binds a user's [W3ID](#web-30-identifier-w3id-ename) and the user's [Public Key](#public-key). It is signed by a [digital] notary participating in PKI. See [eID Wallet](/docs/Infrastructure/eID-Wallet) for how the prototype uses eID and key binding.
Expand All@@ -58,7 +70,7 @@ A non-human object within the MetaState such as an organization, a platform, a b

## Envelope

The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition and ACL that defines who is allowed to access it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.
The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition, and its MetaEnvelope carries the access policy that defines who is allowed to do what with it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.

---

Expand All@@ -68,6 +80,12 @@ A secure storage location or server for the management of data and credentials o

---

## Grant

An entry in an [ACL](#access-control-list-acl) pairing a party's [eName](#web-30-identifier-w3id--ename) with the permissions it holds, as a bitmask of Read, Create, Update and Delete. Where several grants could apply, only the most specific is used — a grant to a user beats one to a platform, which beats one to a group — and less specific grants do not add to it.

---

## Group

An [Entity](#entity): a reference to a number of users within the MetaState that holds its own [W3ID](#web-30-identifier-w3id-ename) and [eVault](#evault). It is often seen as a "group" in social networks.
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions docs/docs/Infrastructure/eVault.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,7 +65,8 @@ A **MetaEnvelope** is the top-level container for an entity (post, user, message

- **id**: Unique identifier (W3ID). Note: Only IDs registered in the Registry are guaranteed to be globally unique.
- **ontology**: Schema identifier (W3ID, e.g., "550e8400-e29b-41d4-a716-446655440001"). Schema W3IDs can be resolved to their schema definitions via the [Ontology](/docs/Infrastructure/Ontology) service. See [W3DS Basics](/docs/W3DS%20Basics/getting-started) for more information on ontology schemas.
- **acl**: Access Control List (who can access this data)
- **acl**: Legacy access control list (who can access this data)
- **_acl**: The granular access policy — grants, denials, and ontology conditions. Takes precedence over `acl` when present. See [Access Control](/docs/W3DS%20Protocol/Access-Control).
- **envelopes**: Array of individual Envelope nodes

### Envelopes
Expand All@@ -83,7 +84,7 @@ Each field in a MetaEnvelope becomes a separate **Envelope** node in Neo4j:
In Neo4j, the structure looks like:

```cypher
(MetaEnvelope {id, ontology, acl}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
(MetaEnvelope {id, ontology, acl, aclBlock}) -[:LINKS_TO]-> (Envelope {id, value, valueType})
```

This flat graph structure allows:
Expand DownExpand Up@@ -603,26 +604,42 @@ curl -X GET "http://localhost:4000/logs?limit=20&cursor=2025-02-04T12:00:00.000Z

## Access Control

eVault uses **Access Control Lists (ACLs)** to determine who can access data.
A MetaEnvelope can carry access rules two ways. The granular `_acl` policy is the current model; the legacy `acl` array predates it and still works.

### ACL Format
### The `_acl` policy

ACLs are arrays of W3IDs or special values:
`_acl` holds grants (an eName plus a READ/CREATE/UPDATE/DELETE bitmask), denials, and Resource Link Ontology conditions. Decisions run in a fixed order — denials, then the most specific grant, then the ontology groups — and the most specific grant wins without unioning less specific ones.

Full model, wire format, and current limits: [Access Control](/docs/W3DS%20Protocol/Access-Control).

It is stored on the MetaEnvelope node as the `aclBlock` property (JSON), so the policy travels with the record when it syncs. No migration is needed to start using it: the property is optional, and a node without one is read through its legacy array exactly as before.

:::caution Rolling back

Once records begin carrying policies, treat the deployment as forward-only. Earlier builds do not read `aclBlock` and fall back to the `acl` array — which platforms write as `["*"]` — so a record an owner had locked down would become world-readable again on a rollback.

:::

### Legacy ACL format

Arrays of W3IDs or special values:

- `["*"]`: Public read access (anyone can read, but only the eVault owner can write)
- `["@user-a.w3id"]`: Only User A can access (read and write)
- `["@user-a.w3id", "@user-b.w3id"]`: User A and User B can access (read and write)

**Prototype Limitation**: In the current prototype implementation, ACLs provide all-or-nothing access. There is no read-only access withoutwrite access (except for `["*"]` which provides read-only access for everyone). More granular permissions are planned for future versions.
The array is all-or-nothing: there is no read-only-without-write except `["*"]`. That is what `_acl` replaces. A record with an `_acl` block ignores its array entirely; a record without one behaves exactly as it always has.

### Access Enforcement

The Access Guard middleware enforces ACLs:
The Access Guard middleware enforces access on every operation, with the permission the operation needs (read for queries, create/update/delete for the corresponding mutations):

1. **Extract W3ID**: From `X-ENAME` header or [Bearer token](/docs/W3DS%20Protocol/Authentication)
2. **Check ACL**: Verify the requesting W3ID is in the MetaEnvelope's ACL
3. **Filter Results**: Remove ACL field from responses (security)
4. **Allow/Deny**: Grant or deny access based on ACL
2. **Check the policy**: If the record carries `_acl`, decide by it. Otherwise fall back to the legacy array.
3. **Filter Results**: Remove the legacy `acl` array from responses; `_acl` is returned as the policy in force
4. **Allow/Deny**

A valid Registry-issued platform token satisfies the *legacy* path — but it does **not** bypass an `_acl` policy. A record carrying a policy is decided by that policy for every caller.

### Special Cases

Expand Down
209 changes: 209 additions & 0 deletions docs/docs/Post Platform Guide/access-control.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
---
sidebar_position: 6
---

# Implementing Access Control

Your platform writes records into a user's eVault. By default those records are wide open — anything that syncs to a platform can be read by it. This page is how you narrow that.

For the model itself — the bitmask, specificity, the decision order — see [Access Control](/docs/W3DS%20Protocol/Access-Control) in the protocol section. This page is the practical side: what to send, what comes back, and what will bite you.

## What you get if you do nothing

The Web3 Adapter writes every record with `acl: ["*"]`. That means anyone, everything, and it is what all existing platform data looks like today.

Nothing about that changes on its own. Records with no `_acl` block keep behaving exactly as they always have, including the part where any platform holding a valid Registry-issued token can reach them. You opt in per record by sending a policy.

## Setting a policy

`_acl` is an optional field on the same inputs you already use.

```graphql
mutation {
createMetaEnvelope(input: {
ontology: "550e8400-e29b-41d4-a716-446655440001"
payload: { content: "…", authorId: "…" }
acl: ["*"]
_acl: {
v: 1
grants: [
{ ename: "@7b9c2e1a-4f30-4c5e-9a21-d8e0f1a2b3c4", perms: 15 }
{ ename: "@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b", perms: 1 }
]
denials: { enames: [], conditions: [] }
default_perms: 0
require: []
}
}) {
metaEnvelope { id }
errors { message }
}
}
```

The owner gets `15` (`0x0F`, everything); one platform gets `1` (`0x01`, read). Nobody else is admitted: `require: []` means no group can pass, so step 3 always refuses.

Send `acl` as well. It is still required by the schema, and it is what any record without a policy falls back to — but where `_acl` is present it is ignored entirely, so its value does not matter.

Available on `createMetaEnvelope`, `storeMetaEnvelope`, `updateMetaEnvelope`, `updateMetaEnvelopeById`, `bulkCreateMetaEnvelopes`, and `uploadFile`.

## Permission values

| Want | `perms` | Hex |
|---|---|---|
| Read only | `1` | `0x01` |
| Read + add, but not edit | `3` | `0x03` |
| Read + edit | `5` | `0x05` |
| Everything | `15` | `0x0F` |

Bits: `1` READ, `2` CREATE, `4` UPDATE, `8` DELETE. Union them.

Two values to avoid sending by accident:

- **`0`** is not "no permissions", it is *no grant at all* — the party falls through to the ontology step as though you had never named them. To actually give someone nothing, leave them out and let the default refuse them.
- **Anything above `15`** is rejected outright. Bits 4–7 are reserved, and a write that sets one fails loudly rather than being quietly narrowed.

## Common shapes

**Owner-only.** Nothing but the owner, no fallback.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

**Public read, owner writes.** The empty group always passes, so anyone reaches `default_perms`.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

This is the closest equivalent of the legacy `["*"]`, except that everyone other than the owner is now read-only rather than able to write.

**Public read, one platform excluded.**

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 } ],
"denials": { "enames": ["@2d4f6a8b-1c3e-4d5f-8a9b-0c1d2e3f4a5b"], "conditions": [] },
"default_perms": 1,
"require": [ [] ] }
```

A denial beats everything, including a grant to the same party. This is how a user shuts out a platform they do not trust without having to enumerate the ones they do.

**Append-only log.** A collaborator may add entries but never rewrite or remove one.

```json
{ "v": 1,
"grants": [ { "ename": "@owner", "perms": 15 },
{ "ename": "@collaborator", "perms": 3 } ],
"denials": { "enames": [], "conditions": [] },
"default_perms": 0,
"require": [] }
```

## Acting on behalf of a user

Your platform's token proves your platform. It says nothing about which of your users a request is for, which matters as soon as a policy grants anything at user level.

Send the user's eName in `X-ON-BEHALF-OF`:

```http
POST /graphql
Authorization: Bearer <your platform token>
X-ENAME: @<vault owner>
X-ON-BEHALF-OF: @<the user you are acting for>
```

That user becomes the party the policy is evaluated against, and your platform is recorded alongside them — so a user grant applies at user specificity while a grant to your platform still applies at platform specificity. Omit the header and your platform is the party.

Two things to be clear about:

- **It is your assertion, not a proof.** The eVault has no way to check it, so it trusts you. That also means it will let you reach what the user was granted, which may be broader than your own grant. Do not send a user's eName on a request that user did not actually initiate.
- **It will not get you past a denial.** Denials match your platform as well as the asserted user, so a policy that excludes your platform still excludes it whatever name you send.

Only `@`-prefixed eNames count as parties. Anything else is ignored rather than treated as an identity.

## Reading a policy back

`_acl` is a field on `MetaEnvelope`:

```graphql
query {
metaEnvelope(id: "…") {
_acl {
grants { ename perms }
denials { enames }
default_perms
}
}
}
```

You always get the policy actually in force. A record written with only `acl: ["*"]` reports `default_perms: 15` behind an always-passing group rather than returning the array, so you can render one consistent view without caring how the record was written.

## Things that will bite you

**A grant is final.** If your platform is named in `grants`, that grant decides the answer on its own. It never falls through to `default_perms` — so a platform granted `1` on a record whose `default_perms` is `15` has read access, not full access. Being named is not always an upgrade.

**The most specific grant wins outright.** A grant to a user beats one to a platform, and they are not combined. If a record grants your platform `15` and the acting user `1`, a request carrying that user identity gets `1`. The platform's broader grant is not consulted.

**A valid platform token does not open a policied record.** It still works on records with no `_acl`. That bypass is exactly what a policy exists to close, so do not rely on your token to reach data a user has locked down — handle the refusal instead.

**Updates preserve the policy.** An `updateMetaEnvelope` that omits `_acl` leaves the stored policy alone rather than clearing it. To change a policy, send the new one in full — it replaces, it does not merge.

**A policy is visible to everyone who can read the record.** `_acl` is returned, not stripped — so your denial list tells any permitted reader which platforms the user excluded, and your grant list tells them who else has access. Do not put anything in a policy you would not show to its readers.

**Refusals look like two different things.** A record you may not touch raises `Access denied`. A record that does not exist for that eName returns `null`. Do not treat the second as the first — retrying will not help, and neither will asking for a different verb.

## The adapter does not do this yet

`EVaultClient` hardcodes `acl: ["*"]` and has no `_acl` parameter, so records written through `handleChange` cannot carry a policy today. To set one, call the eVault GraphQL endpoint directly for that record.

Everything else about your integration is unchanged — mapping, webhooks, and the Awareness Protocol do not interact with the policy. The policy is stored inside the record, so it travels with the data when it syncs, without your webhook controller doing anything.

## Errors you will get

A malformed policy is rejected whole — nothing is quietly dropped and stored in a weaker form. Every message is prefixed `Invalid _acl:` unless noted.

| Message | Cause |
|---|---|
| `bits 4-7 are reserved and must be 0` | A `perms` or `default_perms` above `15`. |
| `expected an unsigned byte` | `perms` was not an integer in 0-255. |
| `each grant needs an ename` | A grant object missing its `ename`. |
| `unknown operator "…"` | A condition `op` outside `>=`, `>`, `<=`, `<`, `==`. |
| `needs a finite numeric value` | A condition `value` that is not a number. |
| `grants must be an array` (and similar) | A container sent as the wrong shape. |
| `Unsupported _acl version: n` | `v` set to anything but `1`. |

Condition errors name the position — `require[0][1]`, `denials.conditions[0]` — so you can find the offending entry directly.

Two runtime outcomes worth distinguishing, neither of which is a validation error:

- **`Access denied`** — the record exists and the policy refused you. Retrying will not help; asking for a different verb might.
- **`null`** — no record with that id for that `X-ENAME`. Not a permissions problem.

List queries behave differently again: a record you may not read is **omitted from the results**, not reported. So a list can come back shorter than you expect with no error and no indication that anything was withheld. Do not treat a list's length as a count of what exists.

## Not usable yet

Two parts of the protocol document are specified but not connected, and a policy relying on them will not behave as written:

- **Groups.** Group membership is not resolved, so a grant or denial naming a group matches nobody. A group grant simply fails to apply; a **group denial silently fails to deny**, which is the dangerous direction. Name parties individually for now.
- **Ontology conditions.** No evaluator is wired in, so any condition fails. A `require` group containing conditions can never pass, and a deny condition always fires and refuses everyone. Until that lands, use only `grants`, `denials.enames`, and `require: []` or `require: [[]]`.

Enforcement is eVault-side. Platforms and the adapter do not evaluate policies themselves, so do not treat a policy as a reason to skip your own authorization checks.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the protocol model and wire format
- [eVault](/docs/Infrastructure/eVault) — where policies are stored and enforced
- [Webhook Controller](/docs/Post%20Platform%20Guide/webhook-controller) — the inbound side, unaffected by policies
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/ai-agent-skill.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---

# AI Agent Skill
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# eCurrency: Accounts and Ledger MetaEnvelopes
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
---

# Registering a Platform eVault
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
---

# PP Auth demonstrator
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
---

# Authenticating your platform
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,6 +63,15 @@ The three gates run in order, and each can only narrow the one before it:

A grant cannot widen a certificate. Permitting `health:Read` to a platform never certified for `health` changes nothing.

## Where the record's own rules fit

The policy above is a signed statement about a *subject* — which platforms an owner will deal with at all. It is not stored in the data it protects.

[Access control](/docs/W3DS%20Protocol/Access-Control) is the other half: an `_acl` block inside each record, naming parties and the verbs they hold, plus ontology conditions admitting platforms that were never named. That block is what the eVault evaluates on each request, and it travels with the record when it syncs.

The two are separate gates and neither can widen the other.

## See also

- [Access Control](/docs/W3DS%20Protocol/Access-Control) — the per-record `_acl` policy the eVault enforces
- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
22 changes: 20 additions & 2 deletions docs/docs/W3DS Basics/glossary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,13 @@ Definitions of key terms used across the W3DS and MetaState documentation. Where

## Access

The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by [ACLs](/docs/Infrastructure/eVault#access-control) and [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.
The ability to retrieve or interact with data or services based on permissions and [authentication](#authentication). In W3DS, access to eVault data is governed by the record's own access policy — [granular access control](/docs/W3DS%20Protocol/Access-Control) or the legacy [ACL](/docs/Infrastructure/eVault#access-control) array — and by [resolution](/docs/Infrastructure/Registry#get-resolve) of identities.

---

## Access Control List (ACL)

The rules stored inside a record saying who may do what with it. Held in the record's `_acl` block as [grants](#grant), [denials](#denial), and ontology conditions, so the rules travel with the data when it syncs rather than living in a table beside it. The older `acl` string array is the same idea without per-verb granularity. See [Access Control](/docs/W3DS%20Protocol/Access-Control).

---

Expand DownExpand Up@@ -38,6 +44,12 @@ A set of data relating to an identifier that is signed by an issuing party (e.g.

---

## Denial

An entry in an [ACL](#access-control-list-acl) that removes access from a party, either by naming its [eName](#web-30-identifier-w3id--ename) or by stating a condition it must clear. A denial overrides any grant — it is the one place where a more specific rule does not win, because deny always does.

---

## eID (ePassport)

A document, similar to X.509, which binds a user's [W3ID](#web-30-identifier-w3id-ename) and the user's [Public Key](#public-key). It is signed by a [digital] notary participating in PKI. See [eID Wallet](/docs/Infrastructure/eID-Wallet) for how the prototype uses eID and key binding.
Expand All@@ -58,7 +70,7 @@ A non-human object within the MetaState such as an organization, a platform, a b

## Envelope

The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition and ACL that defines who is allowed to access it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.
The smallest unit of data in an [eVault](#evault), addressable by its unique identifier and [ontology](/docs/Infrastructure/Ontology) reference. Each envelope has an attached ontological definition, and its MetaEnvelope carries the access policy that defines who is allowed to do what with it. See [eVault — Data Model](/docs/Infrastructure/eVault#data-model) for how Envelopes are stored and used.

---

Expand All@@ -68,6 +80,12 @@ A secure storage location or server for the management of data and credentials o

---

## Grant

An entry in an [ACL](#access-control-list-acl) pairing a party's [eName](#web-30-identifier-w3id--ename) with the permissions it holds, as a bitmask of Read, Create, Update and Delete. Where several grants could apply, only the most specific is used — a grant to a user beats one to a platform, which beats one to a group — and less specific grants do not add to it.

---

## Group

An [Entity](#entity): a reference to a number of users within the MetaState that holds its own [W3ID](#web-30-identifier-w3id-ename) and [eVault](#evault). It is often seen as a "group" in social networks.
Expand Down
Loading
Loading