Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
86bb79a
feat: publish certification level, identity assurance and assessment …
coodos Aug 30, 2026
cdddb7c
feat: add the certification matrix and level computation
coodos Aug 30, 2026
77300ce
feat: derive identity assurance from binding documents
coodos Aug 30, 2026
ca1bfb8
feat: guide certification with the assessment matrix and derived iden…
coodos Aug 30, 2026
10eb8f6
feat: verify reputation from eReputation and move review into an asse…
coodos Aug 30, 2026
0909cc3
feat: score the assessment by geometric mean and hide the level override
coodos Aug 30, 2026
a9a4240
fix: stop a single L0 dimension collapsing the assessment score
coodos Aug 30, 2026
1522f8d
feat: state each assessment option as an answer a reviewer can choose
coodos Aug 30, 2026
5c15b24
fix: make every assessment option readable on its own
coodos Aug 30, 2026
6f0ee87
fix: word code review as work done, not findings implied
coodos Aug 30, 2026
8a54a36
fix: anchor the assessment drawer to the right edge
coodos Aug 30, 2026
008acee
feat: trace a deployment to its certificate with pp-auth
coodos Aug 30, 2026
07627a5
feat: let a vault owner sign the terms platforms must meet
coodos Aug 30, 2026
ed5acc6
feat: demonstrate domain separation across two deployments
coodos Aug 30, 2026
8f84cfc
docs: document platform authentication and owner terms
coodos Aug 30, 2026
96c8175
fix: canonicalise release statements in the order the wallet signed
coodos Aug 30, 2026
e1a892e
fix: show what the evidence supported when the identity floor caps it
coodos Aug 30, 2026
ff07f2f
feat: run the demonstrator on real platforms and your own eVault
coodos Aug 30, 2026
68b23f9
fix: point wallet callbacks at the address the page was served on
coodos Aug 30, 2026
ac17d50
feat: gate requests on what a platform may do, not just what it is
coodos Aug 30, 2026
55cb212
feat: return real records from a permitted read
coodos Aug 30, 2026
e9dedf3
feat: offer every published domain, and permit beside the request
coodos Aug 30, 2026
8451e0e
fix: drop the permission toggles from the request panel
coodos Aug 30, 2026
a875069
fix: record the demonstrator's dependencies in the lockfile
coodos Aug 30, 2026
294f5c3
feat: add platform management transfer
coodos Aug 30, 2026
0a7761e
feat: protect managed platform profiles
coodos Aug 30, 2026
a237a01
docs: document profile management
coodos Aug 30, 2026
77d0a2b
fix: drop the deployment assurance dimension
coodos Aug 30, 2026
db8562e
fix: describe the geometric mean, and reload the matrix when it changes
coodos Aug 30, 2026
32941b7
Merge pull request #1121 from MetaState-Prototype-Project/feat/platfo…
coodos Aug 30, 2026
337cf3e
Merge pull request #1119 from MetaState-Prototype-Project/feat/pp-aut…
coodos Aug 30, 2026
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
75 changes: 75 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
sidebar_position: 9
---

# PP Auth demonstrator

Shows platform authentication and domain separation against the live network: real platforms, real certificates from the association, real deployments, and your own eVault.

```bash
pnpm --filter pp-auth-demo dev
```

Then open **http://localhost:4310** and sign in with your wallet. It needs `PPA_AWARENESS_API_KEY` (or `AWARENESS_API_KEY`) to see the network, and `PUBLIC_REGISTRY_URL` to resolve eVaults.

Nothing is seeded. If the platforms page is empty, nothing has been deployed or certified yet — which is a true statement about the network rather than a failure of the app.

## Platforms

Every platform with a deployment or a certification decision, read live. Under each are the deployments actually running it, with the release and commit they were built from.

**Check it** verifies that deployment's chain of trust, from scratch, against records anyone can read:

| Link | Where the evidence comes from |
|---|---|
| Possession | the deployment itself — see below |
| Deployment authorised | the wallet signature on the deployment's key document, resolved through the registry |
| Bundle integrity | the hashes covered by that same signature |
| Version identity | UUIDv5 arithmetic over the platform eName and version |
| Release authorship | the release proof in the platform's own profile, and its registry key-binding certificate |
| Accreditation | the association's ES256 certificate for that exact version |

Five of the six are checked by reading. **Possession is not** — the deployment's private key never leaves the deployment, so a reader cannot answer a challenge on its behalf. That link reports "not attempted" rather than pretending it failed a check that was never made.

If you hold the key — because you are the person who made that deployment — paste it and the challenge is signed for real. It is kept in memory for that process only: never written to disk, never logged, gone on restart. A wrong key produces a genuine signature that genuinely fails.

## Your data

Your own eVault records, grouped by the domain each schema declares. That grouping is what a certificate is written against, so it is also what decides who sees what.

The table shows every certified platform against every kind of data you hold, decided by the real certificate's domains and your real signed terms, using the same `authorize` an eVault would call. A platform certified for `social`, `finance` and `media` is allowed those and refused everything else — with the reason spelled out. It cannot reach your messages or your files, and nothing it presents will change that.

## Permissions

Being certified for a kind of data is not permission to do anything with it. This tab is where that is settled.

The domain list is the whole published vocabulary, not just what a platform was certified for — the domains it has no business with are listed too, marked as such, because asking for one and watching the certificate refuse it is the case worth seeing.

Grants are managed by the platform through `POST /api/grants`, not set by hand here — the page shows what happens under them. Each change writes an `AccessGrant` into the owner's eVault as a new revision; clearing both operations withdraws the grant rather than deleting it, so the record shows access was taken away rather than never given.

```bash
curl -X POST http://localhost:4310/api/grants \
-H 'Content-Type: application/json' \
-d '{"platformEname":"@…","domain":"social","operations":["read"]}'
```

**Deployment keys go in here, before you try anything.** Possession is the one link a reader cannot establish by looking, so whether the key is present decides what a check can even mean. Enter it and the deployment can answer a challenge for real; leave it out and every request stops at the handshake, which is the correct outcome.

**Try a request** then runs one all the way through — a named deployment, an operation, a domain — and reports which of the three gates decided.

A permitted read is not a verdict: it goes to the eVault and the records it returns are rendered underneath. A refused one fetches nothing, and says so — the eVault is never asked. A permitted write really writes, with text you supply, into a schema belonging to that domain, and then reads the domain back so you can see it landed.

Turn off write and a write is refused while a read still succeeds; withdraw the grant and the refusal changes from "has not been given permission" to "has been withdrawn".

## Your terms

The association says what a platform was found to be; you decide what that is worth. Set the minimum level and any domain refused outright. The reputation service is named in what you sign but is not a choice: there is one on the network today, so asking you to type its address would only be a way to get it wrong.

Signing goes to your wallet. The signing session id **is** the canonical payload of the statement, so what the wallet signs is exactly the digest of your terms — the signature then verifies against the statement on its own, without anyone trusting this app. The terms are published into your own eVault as an `Access Policy` record, world-readable, and the signature is checked again before the write.

Your terms can only narrow a certificate, never widen it. Permitting `finance` does not let a platform reach finance data it was not certified for.

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication)
- [Access Policy](/docs/W3DS%20Basics/Access-Policy)
116 changes: 116 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
---
sidebar_position: 10
---

# Authenticating your platform

Your deployment proves which release it is running, and the eVault decides what that release may touch. This page is the integration.

For the mechanism itself see [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication).

## Install

```bash
pnpm add @metastate-foundation/auth
```

Both halves ship in one package. Deployments import the signer, verifiers import the verifier; nothing stops you doing both, which is what the demonstrator does.

## What your deployment needs

GitW3 produces all of it when you deploy a release. None of it is secret except the private key, which never leaves your process.

```ts
import type { DeploymentIdentity } from "@metastate-foundation/auth/platform";

const identity: DeploymentIdentity = {
privateKey: process.env.DEPLOYMENT_PRIVATE_KEY!, // PKCS#8, base64
evidence: {
deploymentEname, deploymentName, environment,
deployerEname, platformEname, versionEname,
version, releaseTag, commitSha, publicKey,
deploymentKeyDocument, // binding document, bundle-signed
softwareVersionDocument, // binding document, same signature
accreditationJws, // the association's certificate
issuerJwksUri,
submissionProof, // the release proof the association reviewed
},
};
```

Store the private key the way you store any other deployment secret. If it leaks, the holder can authenticate as your deployment until the deployer revokes the key — it is the whole of the possession proof.

## Authenticating

```ts
import { authenticate } from "@metastate-foundation/auth/platform";

const result = await authenticate(identity, "https://vault.example");
```

That fetches a challenge, signs it, and posts the answer. If you want the two steps yourself — to add retries, or to talk to something other than HTTP — use `answerChallenge(identity, challenge)` and send the response however you like.

## Verifying, if you are the eVault

```ts
import {
createChallengeStore,
verifyHandshake,
authorize,
} from "@metastate-foundation/auth/platform";

const challenges = createChallengeStore(); // module scope, not per request

// POST /pp-auth/challenge
const challenge = challenges.issue(ownerEname);

// POST /pp-auth/verify
const chain = await verifyHandshake(response, {
audience: ownerEname,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL!,
store: challenges,
});

if (!chain.ok) {
// chain.links carries all six with a plain-English detail on each.
return refuse(chain.links.find((link) => !link.ok));
}
```

`chain.claim` is what you learned: platform, deployment, version, level, and the domains it may use.

Then the owner's terms, for each record touched:

```ts
const decision = authorize(policy, {
claim: chain.claim,
domain: schema.domain, // the domain the record's ontology declares
reputation: score ? { engine, score } : null,
});

if (!decision.allowed) return refuse(decision.reason);
```

`decision.reason` is written to be shown to a person. `decision.code` is for your logs.

Hold the challenge store at module scope. Issuing from one instance and redeeming in another rejects every legitimate handshake, and under Vite's dev server a module evaluated twice will do exactly that.

## Injection points

Three things are injectable, all defaulting to the ordinary behaviour:

- `verifyWalletSignature` — how a wallet signature is checked. Defaults to `signature-validator` against your registry.
- `resolveJwks` — how a JWKS URI becomes keys. Defaults to a cached remote fetch. Supply your own to pin a key set or to run offline.
- `now` — the clock, for testing time-dependent behaviour.

## Testing your integration

`@metastate-foundation/auth/platform/scenario` mints a complete, self-consistent chain from keys it generates, so you can exercise your verifier without a wallet, a registry or a live association:

```ts
import { createTrustRoots, mintDeployment } from "@metastate-foundation/auth/platform/scenario";
```

Everything it produces is genuinely signed and genuinely verified. What differs is the root: the keys standing in for the deployer, the registry and the association are local. **Never configure a production verifier with roots from this module** — a chain that verifies against them proves your code works, not that a platform is trustworthy.

The [demonstrator](/docs/Post%20Platform%20Guide/pp-auth-demonstrator) is built on it and is the fastest way to see the whole thing move.
68 changes: 68 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
sidebar_position: 6
---

# Access Policy

Certification tells you what a platform was found to be. It does not tell you whether you want to deal with it. That is the eVault owner's decision, and an **access policy** is where they write it down.

It is a signed statement rather than a stored setting, so it travels with the owner and anyone can check it — the eVault enforcing it, a platform working out whether it is even worth asking, or the owner auditing what they agreed to months later.

## What an owner sets

| Term | Meaning |
|---|---|
| `minimumLevel` | The weakest certification level they will deal with. A platform certified below it is refused whatever its certificate grants. |
| `reputationEngine` | Whose reputation scores they accept, as an eName or host. Blank means reputation is not consulted at all. Today the network runs one service, so applications may reasonably fix this rather than ask. |
| `minimumReputation` | The score that engine must report for the platform. Null means no threshold, which is the common case. |
| `allowedDomains` | Null means "whatever the certificate grants" — the ordinary case. A list narrows it further. |
| `deniedDomains` | Refused outright, overriding both the certificate and the allow list. |

Naming the engine matters. A score is only meaningful relative to how it was calculated, so the owner elects which calculation they accept rather than inheriting whichever engine a platform happens to cite. A score from an engine the owner did not name counts as no score at all.

## A policy can only narrow

An owner permitting `finance` does not let a social platform reach finance data. The certificate gate runs first and independently: if `finance` is not in what the association granted the release, nothing in the owner's policy can put it there.

This ordering is the point. The owner's terms are a second lock, not a master key.

## The statement

```json
{
"subject": "@849c0221-6f3f-55f9-95f0-f3b0d2b3092f",
"minimumLevel": "L3",
"reputationEngine": "@ereputation.w3ds",
"minimumReputation": 40,
"allowedDomains": null,
"deniedDomains": ["health"],
"issuedAt": "2026-08-30T16:04:11.230Z",
"nonce": "0f1c…"
}
```

Signed by the owner's wallet over `w3ds:access-policy:v1:` + base64url(sha256(canonical statement)). The signer must be the subject: a policy signed by anyone else is somebody setting terms on a vault that is not theirs, and is rejected.

The newest statement for a subject is the one in force. An owner who has never set one is treated as requiring **L2** — the lowest level the framework issues to a release whose responsible people are identified at all.

Published as the `Access Policy` ontology (`c7a41f6d-95b8-4e2a-9c33-8f0d1b6e4a72`), domain `governance`.

## Permissions are a separate question

A policy says which platforms you will deal with. It does not say what they may *do* — reading your posts is not the same as writing to them.

That is what an [`AccessGrant`](https://github.com/MetaState-Prototype-Project/prototype/blob/main/services/ontology/schemas/accessGrant.json) is for: a grantee, a resource, and permissions written as `resource:Action` (`social:Read`, `finance:Write`). Grants are **deny by default** — a platform that is certified for a domain and permitted by your policy still needs a grant covering the operation it is attempting.

Grants are append-only. Changing what a platform may do writes a new revision rather than editing the old record, and withdrawing access marks the grant revoked while keeping the permissions it used to carry. So "your access was withdrawn" and "you never had access" stay distinguishable, which matters when explaining a refusal to someone.

The three gates run in order, and each can only narrow the one before it:

1. **The certificate** — was this release assessed for this domain?
2. **Your policy** — will you deal with this platform at all?
3. **The grants** — may it do this particular thing?

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

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
2 changes: 1 addition & 1 deletion docs/docs/W3DS Basics/Links.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# Links
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
Show all changes
31 commits
Select commit Hold shift + click to select a range
86bb79a
feat: publish certification level, identity assurance and assessment …
coodos Aug 30, 2026
cdddb7c
feat: add the certification matrix and level computation
coodos Aug 30, 2026
77300ce
feat: derive identity assurance from binding documents
coodos Aug 30, 2026
ca1bfb8
feat: guide certification with the assessment matrix and derived iden…
coodos Aug 30, 2026
10eb8f6
feat: verify reputation from eReputation and move review into an asse…
coodos Aug 30, 2026
0909cc3
feat: score the assessment by geometric mean and hide the level override
coodos Aug 30, 2026
a9a4240
fix: stop a single L0 dimension collapsing the assessment score
coodos Aug 30, 2026
1522f8d
feat: state each assessment option as an answer a reviewer can choose
coodos Aug 30, 2026
5c15b24
fix: make every assessment option readable on its own
coodos Aug 30, 2026
6f0ee87
fix: word code review as work done, not findings implied
coodos Aug 30, 2026
8a54a36
fix: anchor the assessment drawer to the right edge
coodos Aug 30, 2026
008acee
feat: trace a deployment to its certificate with pp-auth
coodos Aug 30, 2026
07627a5
feat: let a vault owner sign the terms platforms must meet
coodos Aug 30, 2026
ed5acc6
feat: demonstrate domain separation across two deployments
coodos Aug 30, 2026
8f84cfc
docs: document platform authentication and owner terms
coodos Aug 30, 2026
96c8175
fix: canonicalise release statements in the order the wallet signed
coodos Aug 30, 2026
e1a892e
fix: show what the evidence supported when the identity floor caps it
coodos Aug 30, 2026
ff07f2f
feat: run the demonstrator on real platforms and your own eVault
coodos Aug 30, 2026
68b23f9
fix: point wallet callbacks at the address the page was served on
coodos Aug 30, 2026
ac17d50
feat: gate requests on what a platform may do, not just what it is
coodos Aug 30, 2026
55cb212
feat: return real records from a permitted read
coodos Aug 30, 2026
e9dedf3
feat: offer every published domain, and permit beside the request
coodos Aug 30, 2026
8451e0e
fix: drop the permission toggles from the request panel
coodos Aug 30, 2026
a875069
fix: record the demonstrator's dependencies in the lockfile
coodos Aug 30, 2026
294f5c3
feat: add platform management transfer
coodos Aug 30, 2026
0a7761e
feat: protect managed platform profiles
coodos Aug 30, 2026
a237a01
docs: document profile management
coodos Aug 30, 2026
77d0a2b
fix: drop the deployment assurance dimension
coodos Aug 30, 2026
db8562e
fix: describe the geometric mean, and reload the matrix when it changes
coodos Aug 30, 2026
32941b7
Merge pull request #1121 from MetaState-Prototype-Project/feat/platfo…
coodos Aug 30, 2026
337cf3e
Merge pull request #1119 from MetaState-Prototype-Project/feat/pp-aut…
coodos Aug 30, 2026
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
75 changes: 75 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
sidebar_position: 9
---

# PP Auth demonstrator

Shows platform authentication and domain separation against the live network: real platforms, real certificates from the association, real deployments, and your own eVault.

```bash
pnpm --filter pp-auth-demo dev
```

Then open **http://localhost:4310** and sign in with your wallet. It needs `PPA_AWARENESS_API_KEY` (or `AWARENESS_API_KEY`) to see the network, and `PUBLIC_REGISTRY_URL` to resolve eVaults.

Nothing is seeded. If the platforms page is empty, nothing has been deployed or certified yet — which is a true statement about the network rather than a failure of the app.

## Platforms

Every platform with a deployment or a certification decision, read live. Under each are the deployments actually running it, with the release and commit they were built from.

**Check it** verifies that deployment's chain of trust, from scratch, against records anyone can read:

| Link | Where the evidence comes from |
|---|---|
| Possession | the deployment itself — see below |
| Deployment authorised | the wallet signature on the deployment's key document, resolved through the registry |
| Bundle integrity | the hashes covered by that same signature |
| Version identity | UUIDv5 arithmetic over the platform eName and version |
| Release authorship | the release proof in the platform's own profile, and its registry key-binding certificate |
| Accreditation | the association's ES256 certificate for that exact version |

Five of the six are checked by reading. **Possession is not** — the deployment's private key never leaves the deployment, so a reader cannot answer a challenge on its behalf. That link reports "not attempted" rather than pretending it failed a check that was never made.

If you hold the key — because you are the person who made that deployment — paste it and the challenge is signed for real. It is kept in memory for that process only: never written to disk, never logged, gone on restart. A wrong key produces a genuine signature that genuinely fails.

## Your data

Your own eVault records, grouped by the domain each schema declares. That grouping is what a certificate is written against, so it is also what decides who sees what.

The table shows every certified platform against every kind of data you hold, decided by the real certificate's domains and your real signed terms, using the same `authorize` an eVault would call. A platform certified for `social`, `finance` and `media` is allowed those and refused everything else — with the reason spelled out. It cannot reach your messages or your files, and nothing it presents will change that.

## Permissions

Being certified for a kind of data is not permission to do anything with it. This tab is where that is settled.

The domain list is the whole published vocabulary, not just what a platform was certified for — the domains it has no business with are listed too, marked as such, because asking for one and watching the certificate refuse it is the case worth seeing.

Grants are managed by the platform through `POST /api/grants`, not set by hand here — the page shows what happens under them. Each change writes an `AccessGrant` into the owner's eVault as a new revision; clearing both operations withdraws the grant rather than deleting it, so the record shows access was taken away rather than never given.

```bash
curl -X POST http://localhost:4310/api/grants \
-H 'Content-Type: application/json' \
-d '{"platformEname":"@…","domain":"social","operations":["read"]}'
```

**Deployment keys go in here, before you try anything.** Possession is the one link a reader cannot establish by looking, so whether the key is present decides what a check can even mean. Enter it and the deployment can answer a challenge for real; leave it out and every request stops at the handshake, which is the correct outcome.

**Try a request** then runs one all the way through — a named deployment, an operation, a domain — and reports which of the three gates decided.

A permitted read is not a verdict: it goes to the eVault and the records it returns are rendered underneath. A refused one fetches nothing, and says so — the eVault is never asked. A permitted write really writes, with text you supply, into a schema belonging to that domain, and then reads the domain back so you can see it landed.

Turn off write and a write is refused while a read still succeeds; withdraw the grant and the refusal changes from "has not been given permission" to "has been withdrawn".

## Your terms

The association says what a platform was found to be; you decide what that is worth. Set the minimum level and any domain refused outright. The reputation service is named in what you sign but is not a choice: there is one on the network today, so asking you to type its address would only be a way to get it wrong.

Signing goes to your wallet. The signing session id **is** the canonical payload of the statement, so what the wallet signs is exactly the digest of your terms — the signature then verifies against the statement on its own, without anyone trusting this app. The terms are published into your own eVault as an `Access Policy` record, world-readable, and the signature is checked again before the write.

Your terms can only narrow a certificate, never widen it. Permitting `finance` does not let a platform reach finance data it was not certified for.

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication)
- [Access Policy](/docs/W3DS%20Basics/Access-Policy)
116 changes: 116 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
---
sidebar_position: 10
---

# Authenticating your platform

Your deployment proves which release it is running, and the eVault decides what that release may touch. This page is the integration.

For the mechanism itself see [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication).

## Install

```bash
pnpm add @metastate-foundation/auth
```

Both halves ship in one package. Deployments import the signer, verifiers import the verifier; nothing stops you doing both, which is what the demonstrator does.

## What your deployment needs

GitW3 produces all of it when you deploy a release. None of it is secret except the private key, which never leaves your process.

```ts
import type { DeploymentIdentity } from "@metastate-foundation/auth/platform";

const identity: DeploymentIdentity = {
privateKey: process.env.DEPLOYMENT_PRIVATE_KEY!, // PKCS#8, base64
evidence: {
deploymentEname, deploymentName, environment,
deployerEname, platformEname, versionEname,
version, releaseTag, commitSha, publicKey,
deploymentKeyDocument, // binding document, bundle-signed
softwareVersionDocument, // binding document, same signature
accreditationJws, // the association's certificate
issuerJwksUri,
submissionProof, // the release proof the association reviewed
},
};
```

Store the private key the way you store any other deployment secret. If it leaks, the holder can authenticate as your deployment until the deployer revokes the key — it is the whole of the possession proof.

## Authenticating

```ts
import { authenticate } from "@metastate-foundation/auth/platform";

const result = await authenticate(identity, "https://vault.example");
```

That fetches a challenge, signs it, and posts the answer. If you want the two steps yourself — to add retries, or to talk to something other than HTTP — use `answerChallenge(identity, challenge)` and send the response however you like.

## Verifying, if you are the eVault

```ts
import {
createChallengeStore,
verifyHandshake,
authorize,
} from "@metastate-foundation/auth/platform";

const challenges = createChallengeStore(); // module scope, not per request

// POST /pp-auth/challenge
const challenge = challenges.issue(ownerEname);

// POST /pp-auth/verify
const chain = await verifyHandshake(response, {
audience: ownerEname,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL!,
store: challenges,
});

if (!chain.ok) {
// chain.links carries all six with a plain-English detail on each.
return refuse(chain.links.find((link) => !link.ok));
}
```

`chain.claim` is what you learned: platform, deployment, version, level, and the domains it may use.

Then the owner's terms, for each record touched:

```ts
const decision = authorize(policy, {
claim: chain.claim,
domain: schema.domain, // the domain the record's ontology declares
reputation: score ? { engine, score } : null,
});

if (!decision.allowed) return refuse(decision.reason);
```

`decision.reason` is written to be shown to a person. `decision.code` is for your logs.

Hold the challenge store at module scope. Issuing from one instance and redeeming in another rejects every legitimate handshake, and under Vite's dev server a module evaluated twice will do exactly that.

## Injection points

Three things are injectable, all defaulting to the ordinary behaviour:

- `verifyWalletSignature` — how a wallet signature is checked. Defaults to `signature-validator` against your registry.
- `resolveJwks` — how a JWKS URI becomes keys. Defaults to a cached remote fetch. Supply your own to pin a key set or to run offline.
- `now` — the clock, for testing time-dependent behaviour.

## Testing your integration

`@metastate-foundation/auth/platform/scenario` mints a complete, self-consistent chain from keys it generates, so you can exercise your verifier without a wallet, a registry or a live association:

```ts
import { createTrustRoots, mintDeployment } from "@metastate-foundation/auth/platform/scenario";
```

Everything it produces is genuinely signed and genuinely verified. What differs is the root: the keys standing in for the deployer, the registry and the association are local. **Never configure a production verifier with roots from this module** — a chain that verifies against them proves your code works, not that a platform is trustworthy.

The [demonstrator](/docs/Post%20Platform%20Guide/pp-auth-demonstrator) is built on it and is the fastest way to see the whole thing move.
68 changes: 68 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
sidebar_position: 6
---

# Access Policy

Certification tells you what a platform was found to be. It does not tell you whether you want to deal with it. That is the eVault owner's decision, and an **access policy** is where they write it down.

It is a signed statement rather than a stored setting, so it travels with the owner and anyone can check it — the eVault enforcing it, a platform working out whether it is even worth asking, or the owner auditing what they agreed to months later.

## What an owner sets

| Term | Meaning |
|---|---|
| `minimumLevel` | The weakest certification level they will deal with. A platform certified below it is refused whatever its certificate grants. |
| `reputationEngine` | Whose reputation scores they accept, as an eName or host. Blank means reputation is not consulted at all. Today the network runs one service, so applications may reasonably fix this rather than ask. |
| `minimumReputation` | The score that engine must report for the platform. Null means no threshold, which is the common case. |
| `allowedDomains` | Null means "whatever the certificate grants" — the ordinary case. A list narrows it further. |
| `deniedDomains` | Refused outright, overriding both the certificate and the allow list. |

Naming the engine matters. A score is only meaningful relative to how it was calculated, so the owner elects which calculation they accept rather than inheriting whichever engine a platform happens to cite. A score from an engine the owner did not name counts as no score at all.

## A policy can only narrow

An owner permitting `finance` does not let a social platform reach finance data. The certificate gate runs first and independently: if `finance` is not in what the association granted the release, nothing in the owner's policy can put it there.

This ordering is the point. The owner's terms are a second lock, not a master key.

## The statement

```json
{
"subject": "@849c0221-6f3f-55f9-95f0-f3b0d2b3092f",
"minimumLevel": "L3",
"reputationEngine": "@ereputation.w3ds",
"minimumReputation": 40,
"allowedDomains": null,
"deniedDomains": ["health"],
"issuedAt": "2026-08-30T16:04:11.230Z",
"nonce": "0f1c…"
}
```

Signed by the owner's wallet over `w3ds:access-policy:v1:` + base64url(sha256(canonical statement)). The signer must be the subject: a policy signed by anyone else is somebody setting terms on a vault that is not theirs, and is rejected.

The newest statement for a subject is the one in force. An owner who has never set one is treated as requiring **L2** — the lowest level the framework issues to a release whose responsible people are identified at all.

Published as the `Access Policy` ontology (`c7a41f6d-95b8-4e2a-9c33-8f0d1b6e4a72`), domain `governance`.

## Permissions are a separate question

A policy says which platforms you will deal with. It does not say what they may *do* — reading your posts is not the same as writing to them.

That is what an [`AccessGrant`](https://github.com/MetaState-Prototype-Project/prototype/blob/main/services/ontology/schemas/accessGrant.json) is for: a grantee, a resource, and permissions written as `resource:Action` (`social:Read`, `finance:Write`). Grants are **deny by default** — a platform that is certified for a domain and permitted by your policy still needs a grant covering the operation it is attempting.

Grants are append-only. Changing what a platform may do writes a new revision rather than editing the old record, and withdrawing access marks the grant revoked while keeping the permissions it used to carry. So "your access was withdrawn" and "you never had access" stay distinguishable, which matters when explaining a refusal to someone.

The three gates run in order, and each can only narrow the one before it:

1. **The certificate** — was this release assessed for this domain?
2. **Your policy** — will you deal with this platform at all?
3. **The grants** — may it do this particular thing?

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

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
2 changes: 1 addition & 1 deletion docs/docs/W3DS Basics/Links.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# Links
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
Show all changes
31 commits
Select commit Hold shift + click to select a range
86bb79a
feat: publish certification level, identity assurance and assessment …
coodos Aug 30, 2026
cdddb7c
feat: add the certification matrix and level computation
coodos Aug 30, 2026
77300ce
feat: derive identity assurance from binding documents
coodos Aug 30, 2026
ca1bfb8
feat: guide certification with the assessment matrix and derived iden…
coodos Aug 30, 2026
10eb8f6
feat: verify reputation from eReputation and move review into an asse…
coodos Aug 30, 2026
0909cc3
feat: score the assessment by geometric mean and hide the level override
coodos Aug 30, 2026
a9a4240
fix: stop a single L0 dimension collapsing the assessment score
coodos Aug 30, 2026
1522f8d
feat: state each assessment option as an answer a reviewer can choose
coodos Aug 30, 2026
5c15b24
fix: make every assessment option readable on its own
coodos Aug 30, 2026
6f0ee87
fix: word code review as work done, not findings implied
coodos Aug 30, 2026
8a54a36
fix: anchor the assessment drawer to the right edge
coodos Aug 30, 2026
008acee
feat: trace a deployment to its certificate with pp-auth
coodos Aug 30, 2026
07627a5
feat: let a vault owner sign the terms platforms must meet
coodos Aug 30, 2026
ed5acc6
feat: demonstrate domain separation across two deployments
coodos Aug 30, 2026
8f84cfc
docs: document platform authentication and owner terms
coodos Aug 30, 2026
96c8175
fix: canonicalise release statements in the order the wallet signed
coodos Aug 30, 2026
e1a892e
fix: show what the evidence supported when the identity floor caps it
coodos Aug 30, 2026
ff07f2f
feat: run the demonstrator on real platforms and your own eVault
coodos Aug 30, 2026
68b23f9
fix: point wallet callbacks at the address the page was served on
coodos Aug 30, 2026
ac17d50
feat: gate requests on what a platform may do, not just what it is
coodos Aug 30, 2026
55cb212
feat: return real records from a permitted read
coodos Aug 30, 2026
e9dedf3
feat: offer every published domain, and permit beside the request
coodos Aug 30, 2026
8451e0e
fix: drop the permission toggles from the request panel
coodos Aug 30, 2026
a875069
fix: record the demonstrator's dependencies in the lockfile
coodos Aug 30, 2026
294f5c3
feat: add platform management transfer
coodos Aug 30, 2026
0a7761e
feat: protect managed platform profiles
coodos Aug 30, 2026
a237a01
docs: document profile management
coodos Aug 30, 2026
77d0a2b
fix: drop the deployment assurance dimension
coodos Aug 30, 2026
db8562e
fix: describe the geometric mean, and reload the matrix when it changes
coodos Aug 30, 2026
32941b7
Merge pull request #1121 from MetaState-Prototype-Project/feat/platfo…
coodos Aug 30, 2026
337cf3e
Merge pull request #1119 from MetaState-Prototype-Project/feat/pp-aut…
coodos Aug 30, 2026
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
75 changes: 75 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
sidebar_position: 9
---

# PP Auth demonstrator

Shows platform authentication and domain separation against the live network: real platforms, real certificates from the association, real deployments, and your own eVault.

```bash
pnpm --filter pp-auth-demo dev
```

Then open **http://localhost:4310** and sign in with your wallet. It needs `PPA_AWARENESS_API_KEY` (or `AWARENESS_API_KEY`) to see the network, and `PUBLIC_REGISTRY_URL` to resolve eVaults.

Nothing is seeded. If the platforms page is empty, nothing has been deployed or certified yet — which is a true statement about the network rather than a failure of the app.

## Platforms

Every platform with a deployment or a certification decision, read live. Under each are the deployments actually running it, with the release and commit they were built from.

**Check it** verifies that deployment's chain of trust, from scratch, against records anyone can read:

| Link | Where the evidence comes from |
|---|---|
| Possession | the deployment itself — see below |
| Deployment authorised | the wallet signature on the deployment's key document, resolved through the registry |
| Bundle integrity | the hashes covered by that same signature |
| Version identity | UUIDv5 arithmetic over the platform eName and version |
| Release authorship | the release proof in the platform's own profile, and its registry key-binding certificate |
| Accreditation | the association's ES256 certificate for that exact version |

Five of the six are checked by reading. **Possession is not** — the deployment's private key never leaves the deployment, so a reader cannot answer a challenge on its behalf. That link reports "not attempted" rather than pretending it failed a check that was never made.

If you hold the key — because you are the person who made that deployment — paste it and the challenge is signed for real. It is kept in memory for that process only: never written to disk, never logged, gone on restart. A wrong key produces a genuine signature that genuinely fails.

## Your data

Your own eVault records, grouped by the domain each schema declares. That grouping is what a certificate is written against, so it is also what decides who sees what.

The table shows every certified platform against every kind of data you hold, decided by the real certificate's domains and your real signed terms, using the same `authorize` an eVault would call. A platform certified for `social`, `finance` and `media` is allowed those and refused everything else — with the reason spelled out. It cannot reach your messages or your files, and nothing it presents will change that.

## Permissions

Being certified for a kind of data is not permission to do anything with it. This tab is where that is settled.

The domain list is the whole published vocabulary, not just what a platform was certified for — the domains it has no business with are listed too, marked as such, because asking for one and watching the certificate refuse it is the case worth seeing.

Grants are managed by the platform through `POST /api/grants`, not set by hand here — the page shows what happens under them. Each change writes an `AccessGrant` into the owner's eVault as a new revision; clearing both operations withdraws the grant rather than deleting it, so the record shows access was taken away rather than never given.

```bash
curl -X POST http://localhost:4310/api/grants \
-H 'Content-Type: application/json' \
-d '{"platformEname":"@…","domain":"social","operations":["read"]}'
```

**Deployment keys go in here, before you try anything.** Possession is the one link a reader cannot establish by looking, so whether the key is present decides what a check can even mean. Enter it and the deployment can answer a challenge for real; leave it out and every request stops at the handshake, which is the correct outcome.

**Try a request** then runs one all the way through — a named deployment, an operation, a domain — and reports which of the three gates decided.

A permitted read is not a verdict: it goes to the eVault and the records it returns are rendered underneath. A refused one fetches nothing, and says so — the eVault is never asked. A permitted write really writes, with text you supply, into a schema belonging to that domain, and then reads the domain back so you can see it landed.

Turn off write and a write is refused while a read still succeeds; withdraw the grant and the refusal changes from "has not been given permission" to "has been withdrawn".

## Your terms

The association says what a platform was found to be; you decide what that is worth. Set the minimum level and any domain refused outright. The reputation service is named in what you sign but is not a choice: there is one on the network today, so asking you to type its address would only be a way to get it wrong.

Signing goes to your wallet. The signing session id **is** the canonical payload of the statement, so what the wallet signs is exactly the digest of your terms — the signature then verifies against the statement on its own, without anyone trusting this app. The terms are published into your own eVault as an `Access Policy` record, world-readable, and the signature is checked again before the write.

Your terms can only narrow a certificate, never widen it. Permitting `finance` does not let a platform reach finance data it was not certified for.

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication)
- [Access Policy](/docs/W3DS%20Basics/Access-Policy)
116 changes: 116 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
---
sidebar_position: 10
---

# Authenticating your platform

Your deployment proves which release it is running, and the eVault decides what that release may touch. This page is the integration.

For the mechanism itself see [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication).

## Install

```bash
pnpm add @metastate-foundation/auth
```

Both halves ship in one package. Deployments import the signer, verifiers import the verifier; nothing stops you doing both, which is what the demonstrator does.

## What your deployment needs

GitW3 produces all of it when you deploy a release. None of it is secret except the private key, which never leaves your process.

```ts
import type { DeploymentIdentity } from "@metastate-foundation/auth/platform";

const identity: DeploymentIdentity = {
privateKey: process.env.DEPLOYMENT_PRIVATE_KEY!, // PKCS#8, base64
evidence: {
deploymentEname, deploymentName, environment,
deployerEname, platformEname, versionEname,
version, releaseTag, commitSha, publicKey,
deploymentKeyDocument, // binding document, bundle-signed
softwareVersionDocument, // binding document, same signature
accreditationJws, // the association's certificate
issuerJwksUri,
submissionProof, // the release proof the association reviewed
},
};
```

Store the private key the way you store any other deployment secret. If it leaks, the holder can authenticate as your deployment until the deployer revokes the key — it is the whole of the possession proof.

## Authenticating

```ts
import { authenticate } from "@metastate-foundation/auth/platform";

const result = await authenticate(identity, "https://vault.example");
```

That fetches a challenge, signs it, and posts the answer. If you want the two steps yourself — to add retries, or to talk to something other than HTTP — use `answerChallenge(identity, challenge)` and send the response however you like.

## Verifying, if you are the eVault

```ts
import {
createChallengeStore,
verifyHandshake,
authorize,
} from "@metastate-foundation/auth/platform";

const challenges = createChallengeStore(); // module scope, not per request

// POST /pp-auth/challenge
const challenge = challenges.issue(ownerEname);

// POST /pp-auth/verify
const chain = await verifyHandshake(response, {
audience: ownerEname,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL!,
store: challenges,
});

if (!chain.ok) {
// chain.links carries all six with a plain-English detail on each.
return refuse(chain.links.find((link) => !link.ok));
}
```

`chain.claim` is what you learned: platform, deployment, version, level, and the domains it may use.

Then the owner's terms, for each record touched:

```ts
const decision = authorize(policy, {
claim: chain.claim,
domain: schema.domain, // the domain the record's ontology declares
reputation: score ? { engine, score } : null,
});

if (!decision.allowed) return refuse(decision.reason);
```

`decision.reason` is written to be shown to a person. `decision.code` is for your logs.

Hold the challenge store at module scope. Issuing from one instance and redeeming in another rejects every legitimate handshake, and under Vite's dev server a module evaluated twice will do exactly that.

## Injection points

Three things are injectable, all defaulting to the ordinary behaviour:

- `verifyWalletSignature` — how a wallet signature is checked. Defaults to `signature-validator` against your registry.
- `resolveJwks` — how a JWKS URI becomes keys. Defaults to a cached remote fetch. Supply your own to pin a key set or to run offline.
- `now` — the clock, for testing time-dependent behaviour.

## Testing your integration

`@metastate-foundation/auth/platform/scenario` mints a complete, self-consistent chain from keys it generates, so you can exercise your verifier without a wallet, a registry or a live association:

```ts
import { createTrustRoots, mintDeployment } from "@metastate-foundation/auth/platform/scenario";
```

Everything it produces is genuinely signed and genuinely verified. What differs is the root: the keys standing in for the deployer, the registry and the association are local. **Never configure a production verifier with roots from this module** — a chain that verifies against them proves your code works, not that a platform is trustworthy.

The [demonstrator](/docs/Post%20Platform%20Guide/pp-auth-demonstrator) is built on it and is the fastest way to see the whole thing move.
68 changes: 68 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
sidebar_position: 6
---

# Access Policy

Certification tells you what a platform was found to be. It does not tell you whether you want to deal with it. That is the eVault owner's decision, and an **access policy** is where they write it down.

It is a signed statement rather than a stored setting, so it travels with the owner and anyone can check it — the eVault enforcing it, a platform working out whether it is even worth asking, or the owner auditing what they agreed to months later.

## What an owner sets

| Term | Meaning |
|---|---|
| `minimumLevel` | The weakest certification level they will deal with. A platform certified below it is refused whatever its certificate grants. |
| `reputationEngine` | Whose reputation scores they accept, as an eName or host. Blank means reputation is not consulted at all. Today the network runs one service, so applications may reasonably fix this rather than ask. |
| `minimumReputation` | The score that engine must report for the platform. Null means no threshold, which is the common case. |
| `allowedDomains` | Null means "whatever the certificate grants" — the ordinary case. A list narrows it further. |
| `deniedDomains` | Refused outright, overriding both the certificate and the allow list. |

Naming the engine matters. A score is only meaningful relative to how it was calculated, so the owner elects which calculation they accept rather than inheriting whichever engine a platform happens to cite. A score from an engine the owner did not name counts as no score at all.

## A policy can only narrow

An owner permitting `finance` does not let a social platform reach finance data. The certificate gate runs first and independently: if `finance` is not in what the association granted the release, nothing in the owner's policy can put it there.

This ordering is the point. The owner's terms are a second lock, not a master key.

## The statement

```json
{
"subject": "@849c0221-6f3f-55f9-95f0-f3b0d2b3092f",
"minimumLevel": "L3",
"reputationEngine": "@ereputation.w3ds",
"minimumReputation": 40,
"allowedDomains": null,
"deniedDomains": ["health"],
"issuedAt": "2026-08-30T16:04:11.230Z",
"nonce": "0f1c…"
}
```

Signed by the owner's wallet over `w3ds:access-policy:v1:` + base64url(sha256(canonical statement)). The signer must be the subject: a policy signed by anyone else is somebody setting terms on a vault that is not theirs, and is rejected.

The newest statement for a subject is the one in force. An owner who has never set one is treated as requiring **L2** — the lowest level the framework issues to a release whose responsible people are identified at all.

Published as the `Access Policy` ontology (`c7a41f6d-95b8-4e2a-9c33-8f0d1b6e4a72`), domain `governance`.

## Permissions are a separate question

A policy says which platforms you will deal with. It does not say what they may *do* — reading your posts is not the same as writing to them.

That is what an [`AccessGrant`](https://github.com/MetaState-Prototype-Project/prototype/blob/main/services/ontology/schemas/accessGrant.json) is for: a grantee, a resource, and permissions written as `resource:Action` (`social:Read`, `finance:Write`). Grants are **deny by default** — a platform that is certified for a domain and permitted by your policy still needs a grant covering the operation it is attempting.

Grants are append-only. Changing what a platform may do writes a new revision rather than editing the old record, and withdrawing access marks the grant revoked while keeping the permissions it used to carry. So "your access was withdrawn" and "you never had access" stay distinguishable, which matters when explaining a refusal to someone.

The three gates run in order, and each can only narrow the one before it:

1. **The certificate** — was this release assessed for this domain?
2. **Your policy** — will you deal with this platform at all?
3. **The grants** — may it do this particular thing?

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

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
2 changes: 1 addition & 1 deletion docs/docs/W3DS Basics/Links.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# Links
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
Show all changes
31 commits
Select commit Hold shift + click to select a range
86bb79a
feat: publish certification level, identity assurance and assessment …
coodos Aug 30, 2026
cdddb7c
feat: add the certification matrix and level computation
coodos Aug 30, 2026
77300ce
feat: derive identity assurance from binding documents
coodos Aug 30, 2026
ca1bfb8
feat: guide certification with the assessment matrix and derived iden…
coodos Aug 30, 2026
10eb8f6
feat: verify reputation from eReputation and move review into an asse…
coodos Aug 30, 2026
0909cc3
feat: score the assessment by geometric mean and hide the level override
coodos Aug 30, 2026
a9a4240
fix: stop a single L0 dimension collapsing the assessment score
coodos Aug 30, 2026
1522f8d
feat: state each assessment option as an answer a reviewer can choose
coodos Aug 30, 2026
5c15b24
fix: make every assessment option readable on its own
coodos Aug 30, 2026
6f0ee87
fix: word code review as work done, not findings implied
coodos Aug 30, 2026
8a54a36
fix: anchor the assessment drawer to the right edge
coodos Aug 30, 2026
008acee
feat: trace a deployment to its certificate with pp-auth
coodos Aug 30, 2026
07627a5
feat: let a vault owner sign the terms platforms must meet
coodos Aug 30, 2026
ed5acc6
feat: demonstrate domain separation across two deployments
coodos Aug 30, 2026
8f84cfc
docs: document platform authentication and owner terms
coodos Aug 30, 2026
96c8175
fix: canonicalise release statements in the order the wallet signed
coodos Aug 30, 2026
e1a892e
fix: show what the evidence supported when the identity floor caps it
coodos Aug 30, 2026
ff07f2f
feat: run the demonstrator on real platforms and your own eVault
coodos Aug 30, 2026
68b23f9
fix: point wallet callbacks at the address the page was served on
coodos Aug 30, 2026
ac17d50
feat: gate requests on what a platform may do, not just what it is
coodos Aug 30, 2026
55cb212
feat: return real records from a permitted read
coodos Aug 30, 2026
e9dedf3
feat: offer every published domain, and permit beside the request
coodos Aug 30, 2026
8451e0e
fix: drop the permission toggles from the request panel
coodos Aug 30, 2026
a875069
fix: record the demonstrator's dependencies in the lockfile
coodos Aug 30, 2026
294f5c3
feat: add platform management transfer
coodos Aug 30, 2026
0a7761e
feat: protect managed platform profiles
coodos Aug 30, 2026
a237a01
docs: document profile management
coodos Aug 30, 2026
77d0a2b
fix: drop the deployment assurance dimension
coodos Aug 30, 2026
db8562e
fix: describe the geometric mean, and reload the matrix when it changes
coodos Aug 30, 2026
32941b7
Merge pull request #1121 from MetaState-Prototype-Project/feat/platfo…
coodos Aug 30, 2026
337cf3e
Merge pull request #1119 from MetaState-Prototype-Project/feat/pp-aut…
coodos Aug 30, 2026
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
75 changes: 75 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
sidebar_position: 9
---

# PP Auth demonstrator

Shows platform authentication and domain separation against the live network: real platforms, real certificates from the association, real deployments, and your own eVault.

```bash
pnpm --filter pp-auth-demo dev
```

Then open **http://localhost:4310** and sign in with your wallet. It needs `PPA_AWARENESS_API_KEY` (or `AWARENESS_API_KEY`) to see the network, and `PUBLIC_REGISTRY_URL` to resolve eVaults.

Nothing is seeded. If the platforms page is empty, nothing has been deployed or certified yet — which is a true statement about the network rather than a failure of the app.

## Platforms

Every platform with a deployment or a certification decision, read live. Under each are the deployments actually running it, with the release and commit they were built from.

**Check it** verifies that deployment's chain of trust, from scratch, against records anyone can read:

| Link | Where the evidence comes from |
|---|---|
| Possession | the deployment itself — see below |
| Deployment authorised | the wallet signature on the deployment's key document, resolved through the registry |
| Bundle integrity | the hashes covered by that same signature |
| Version identity | UUIDv5 arithmetic over the platform eName and version |
| Release authorship | the release proof in the platform's own profile, and its registry key-binding certificate |
| Accreditation | the association's ES256 certificate for that exact version |

Five of the six are checked by reading. **Possession is not** — the deployment's private key never leaves the deployment, so a reader cannot answer a challenge on its behalf. That link reports "not attempted" rather than pretending it failed a check that was never made.

If you hold the key — because you are the person who made that deployment — paste it and the challenge is signed for real. It is kept in memory for that process only: never written to disk, never logged, gone on restart. A wrong key produces a genuine signature that genuinely fails.

## Your data

Your own eVault records, grouped by the domain each schema declares. That grouping is what a certificate is written against, so it is also what decides who sees what.

The table shows every certified platform against every kind of data you hold, decided by the real certificate's domains and your real signed terms, using the same `authorize` an eVault would call. A platform certified for `social`, `finance` and `media` is allowed those and refused everything else — with the reason spelled out. It cannot reach your messages or your files, and nothing it presents will change that.

## Permissions

Being certified for a kind of data is not permission to do anything with it. This tab is where that is settled.

The domain list is the whole published vocabulary, not just what a platform was certified for — the domains it has no business with are listed too, marked as such, because asking for one and watching the certificate refuse it is the case worth seeing.

Grants are managed by the platform through `POST /api/grants`, not set by hand here — the page shows what happens under them. Each change writes an `AccessGrant` into the owner's eVault as a new revision; clearing both operations withdraws the grant rather than deleting it, so the record shows access was taken away rather than never given.

```bash
curl -X POST http://localhost:4310/api/grants \
-H 'Content-Type: application/json' \
-d '{"platformEname":"@…","domain":"social","operations":["read"]}'
```

**Deployment keys go in here, before you try anything.** Possession is the one link a reader cannot establish by looking, so whether the key is present decides what a check can even mean. Enter it and the deployment can answer a challenge for real; leave it out and every request stops at the handshake, which is the correct outcome.

**Try a request** then runs one all the way through — a named deployment, an operation, a domain — and reports which of the three gates decided.

A permitted read is not a verdict: it goes to the eVault and the records it returns are rendered underneath. A refused one fetches nothing, and says so — the eVault is never asked. A permitted write really writes, with text you supply, into a schema belonging to that domain, and then reads the domain back so you can see it landed.

Turn off write and a write is refused while a read still succeeds; withdraw the grant and the refusal changes from "has not been given permission" to "has been withdrawn".

## Your terms

The association says what a platform was found to be; you decide what that is worth. Set the minimum level and any domain refused outright. The reputation service is named in what you sign but is not a choice: there is one on the network today, so asking you to type its address would only be a way to get it wrong.

Signing goes to your wallet. The signing session id **is** the canonical payload of the statement, so what the wallet signs is exactly the digest of your terms — the signature then verifies against the statement on its own, without anyone trusting this app. The terms are published into your own eVault as an `Access Policy` record, world-readable, and the signature is checked again before the write.

Your terms can only narrow a certificate, never widen it. Permitting `finance` does not let a platform reach finance data it was not certified for.

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication)
- [Access Policy](/docs/W3DS%20Basics/Access-Policy)
116 changes: 116 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
---
sidebar_position: 10
---

# Authenticating your platform

Your deployment proves which release it is running, and the eVault decides what that release may touch. This page is the integration.

For the mechanism itself see [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication).

## Install

```bash
pnpm add @metastate-foundation/auth
```

Both halves ship in one package. Deployments import the signer, verifiers import the verifier; nothing stops you doing both, which is what the demonstrator does.

## What your deployment needs

GitW3 produces all of it when you deploy a release. None of it is secret except the private key, which never leaves your process.

```ts
import type { DeploymentIdentity } from "@metastate-foundation/auth/platform";

const identity: DeploymentIdentity = {
privateKey: process.env.DEPLOYMENT_PRIVATE_KEY!, // PKCS#8, base64
evidence: {
deploymentEname, deploymentName, environment,
deployerEname, platformEname, versionEname,
version, releaseTag, commitSha, publicKey,
deploymentKeyDocument, // binding document, bundle-signed
softwareVersionDocument, // binding document, same signature
accreditationJws, // the association's certificate
issuerJwksUri,
submissionProof, // the release proof the association reviewed
},
};
```

Store the private key the way you store any other deployment secret. If it leaks, the holder can authenticate as your deployment until the deployer revokes the key — it is the whole of the possession proof.

## Authenticating

```ts
import { authenticate } from "@metastate-foundation/auth/platform";

const result = await authenticate(identity, "https://vault.example");
```

That fetches a challenge, signs it, and posts the answer. If you want the two steps yourself — to add retries, or to talk to something other than HTTP — use `answerChallenge(identity, challenge)` and send the response however you like.

## Verifying, if you are the eVault

```ts
import {
createChallengeStore,
verifyHandshake,
authorize,
} from "@metastate-foundation/auth/platform";

const challenges = createChallengeStore(); // module scope, not per request

// POST /pp-auth/challenge
const challenge = challenges.issue(ownerEname);

// POST /pp-auth/verify
const chain = await verifyHandshake(response, {
audience: ownerEname,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL!,
store: challenges,
});

if (!chain.ok) {
// chain.links carries all six with a plain-English detail on each.
return refuse(chain.links.find((link) => !link.ok));
}
```

`chain.claim` is what you learned: platform, deployment, version, level, and the domains it may use.

Then the owner's terms, for each record touched:

```ts
const decision = authorize(policy, {
claim: chain.claim,
domain: schema.domain, // the domain the record's ontology declares
reputation: score ? { engine, score } : null,
});

if (!decision.allowed) return refuse(decision.reason);
```

`decision.reason` is written to be shown to a person. `decision.code` is for your logs.

Hold the challenge store at module scope. Issuing from one instance and redeeming in another rejects every legitimate handshake, and under Vite's dev server a module evaluated twice will do exactly that.

## Injection points

Three things are injectable, all defaulting to the ordinary behaviour:

- `verifyWalletSignature` — how a wallet signature is checked. Defaults to `signature-validator` against your registry.
- `resolveJwks` — how a JWKS URI becomes keys. Defaults to a cached remote fetch. Supply your own to pin a key set or to run offline.
- `now` — the clock, for testing time-dependent behaviour.

## Testing your integration

`@metastate-foundation/auth/platform/scenario` mints a complete, self-consistent chain from keys it generates, so you can exercise your verifier without a wallet, a registry or a live association:

```ts
import { createTrustRoots, mintDeployment } from "@metastate-foundation/auth/platform/scenario";
```

Everything it produces is genuinely signed and genuinely verified. What differs is the root: the keys standing in for the deployer, the registry and the association are local. **Never configure a production verifier with roots from this module** — a chain that verifies against them proves your code works, not that a platform is trustworthy.

The [demonstrator](/docs/Post%20Platform%20Guide/pp-auth-demonstrator) is built on it and is the fastest way to see the whole thing move.
68 changes: 68 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
sidebar_position: 6
---

# Access Policy

Certification tells you what a platform was found to be. It does not tell you whether you want to deal with it. That is the eVault owner's decision, and an **access policy** is where they write it down.

It is a signed statement rather than a stored setting, so it travels with the owner and anyone can check it — the eVault enforcing it, a platform working out whether it is even worth asking, or the owner auditing what they agreed to months later.

## What an owner sets

| Term | Meaning |
|---|---|
| `minimumLevel` | The weakest certification level they will deal with. A platform certified below it is refused whatever its certificate grants. |
| `reputationEngine` | Whose reputation scores they accept, as an eName or host. Blank means reputation is not consulted at all. Today the network runs one service, so applications may reasonably fix this rather than ask. |
| `minimumReputation` | The score that engine must report for the platform. Null means no threshold, which is the common case. |
| `allowedDomains` | Null means "whatever the certificate grants" — the ordinary case. A list narrows it further. |
| `deniedDomains` | Refused outright, overriding both the certificate and the allow list. |

Naming the engine matters. A score is only meaningful relative to how it was calculated, so the owner elects which calculation they accept rather than inheriting whichever engine a platform happens to cite. A score from an engine the owner did not name counts as no score at all.

## A policy can only narrow

An owner permitting `finance` does not let a social platform reach finance data. The certificate gate runs first and independently: if `finance` is not in what the association granted the release, nothing in the owner's policy can put it there.

This ordering is the point. The owner's terms are a second lock, not a master key.

## The statement

```json
{
"subject": "@849c0221-6f3f-55f9-95f0-f3b0d2b3092f",
"minimumLevel": "L3",
"reputationEngine": "@ereputation.w3ds",
"minimumReputation": 40,
"allowedDomains": null,
"deniedDomains": ["health"],
"issuedAt": "2026-08-30T16:04:11.230Z",
"nonce": "0f1c…"
}
```

Signed by the owner's wallet over `w3ds:access-policy:v1:` + base64url(sha256(canonical statement)). The signer must be the subject: a policy signed by anyone else is somebody setting terms on a vault that is not theirs, and is rejected.

The newest statement for a subject is the one in force. An owner who has never set one is treated as requiring **L2** — the lowest level the framework issues to a release whose responsible people are identified at all.

Published as the `Access Policy` ontology (`c7a41f6d-95b8-4e2a-9c33-8f0d1b6e4a72`), domain `governance`.

## Permissions are a separate question

A policy says which platforms you will deal with. It does not say what they may *do* — reading your posts is not the same as writing to them.

That is what an [`AccessGrant`](https://github.com/MetaState-Prototype-Project/prototype/blob/main/services/ontology/schemas/accessGrant.json) is for: a grantee, a resource, and permissions written as `resource:Action` (`social:Read`, `finance:Write`). Grants are **deny by default** — a platform that is certified for a domain and permitted by your policy still needs a grant covering the operation it is attempting.

Grants are append-only. Changing what a platform may do writes a new revision rather than editing the old record, and withdrawing access marks the grant revoked while keeping the permissions it used to carry. So "your access was withdrawn" and "you never had access" stay distinguishable, which matters when explaining a refusal to someone.

The three gates run in order, and each can only narrow the one before it:

1. **The certificate** — was this release assessed for this domain?
2. **Your policy** — will you deal with this platform at all?
3. **The grants** — may it do this particular thing?

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

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
2 changes: 1 addition & 1 deletion docs/docs/W3DS Basics/Links.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# Links
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
Show all changes
31 commits
Select commit Hold shift + click to select a range
86bb79a
feat: publish certification level, identity assurance and assessment …
coodos Aug 30, 2026
cdddb7c
feat: add the certification matrix and level computation
coodos Aug 30, 2026
77300ce
feat: derive identity assurance from binding documents
coodos Aug 30, 2026
ca1bfb8
feat: guide certification with the assessment matrix and derived iden…
coodos Aug 30, 2026
10eb8f6
feat: verify reputation from eReputation and move review into an asse…
coodos Aug 30, 2026
0909cc3
feat: score the assessment by geometric mean and hide the level override
coodos Aug 30, 2026
a9a4240
fix: stop a single L0 dimension collapsing the assessment score
coodos Aug 30, 2026
1522f8d
feat: state each assessment option as an answer a reviewer can choose
coodos Aug 30, 2026
5c15b24
fix: make every assessment option readable on its own
coodos Aug 30, 2026
6f0ee87
fix: word code review as work done, not findings implied
coodos Aug 30, 2026
8a54a36
fix: anchor the assessment drawer to the right edge
coodos Aug 30, 2026
008acee
feat: trace a deployment to its certificate with pp-auth
coodos Aug 30, 2026
07627a5
feat: let a vault owner sign the terms platforms must meet
coodos Aug 30, 2026
ed5acc6
feat: demonstrate domain separation across two deployments
coodos Aug 30, 2026
8f84cfc
docs: document platform authentication and owner terms
coodos Aug 30, 2026
96c8175
fix: canonicalise release statements in the order the wallet signed
coodos Aug 30, 2026
e1a892e
fix: show what the evidence supported when the identity floor caps it
coodos Aug 30, 2026
ff07f2f
feat: run the demonstrator on real platforms and your own eVault
coodos Aug 30, 2026
68b23f9
fix: point wallet callbacks at the address the page was served on
coodos Aug 30, 2026
ac17d50
feat: gate requests on what a platform may do, not just what it is
coodos Aug 30, 2026
55cb212
feat: return real records from a permitted read
coodos Aug 30, 2026
e9dedf3
feat: offer every published domain, and permit beside the request
coodos Aug 30, 2026
8451e0e
fix: drop the permission toggles from the request panel
coodos Aug 30, 2026
a875069
fix: record the demonstrator's dependencies in the lockfile
coodos Aug 30, 2026
294f5c3
feat: add platform management transfer
coodos Aug 30, 2026
0a7761e
feat: protect managed platform profiles
coodos Aug 30, 2026
a237a01
docs: document profile management
coodos Aug 30, 2026
77d0a2b
fix: drop the deployment assurance dimension
coodos Aug 30, 2026
db8562e
fix: describe the geometric mean, and reload the matrix when it changes
coodos Aug 30, 2026
32941b7
Merge pull request #1121 from MetaState-Prototype-Project/feat/platfo…
coodos Aug 30, 2026
337cf3e
Merge pull request #1119 from MetaState-Prototype-Project/feat/pp-aut…
coodos Aug 30, 2026
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
75 changes: 75 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
sidebar_position: 9
---

# PP Auth demonstrator

Shows platform authentication and domain separation against the live network: real platforms, real certificates from the association, real deployments, and your own eVault.

```bash
pnpm --filter pp-auth-demo dev
```

Then open **http://localhost:4310** and sign in with your wallet. It needs `PPA_AWARENESS_API_KEY` (or `AWARENESS_API_KEY`) to see the network, and `PUBLIC_REGISTRY_URL` to resolve eVaults.

Nothing is seeded. If the platforms page is empty, nothing has been deployed or certified yet — which is a true statement about the network rather than a failure of the app.

## Platforms

Every platform with a deployment or a certification decision, read live. Under each are the deployments actually running it, with the release and commit they were built from.

**Check it** verifies that deployment's chain of trust, from scratch, against records anyone can read:

| Link | Where the evidence comes from |
|---|---|
| Possession | the deployment itself — see below |
| Deployment authorised | the wallet signature on the deployment's key document, resolved through the registry |
| Bundle integrity | the hashes covered by that same signature |
| Version identity | UUIDv5 arithmetic over the platform eName and version |
| Release authorship | the release proof in the platform's own profile, and its registry key-binding certificate |
| Accreditation | the association's ES256 certificate for that exact version |

Five of the six are checked by reading. **Possession is not** — the deployment's private key never leaves the deployment, so a reader cannot answer a challenge on its behalf. That link reports "not attempted" rather than pretending it failed a check that was never made.

If you hold the key — because you are the person who made that deployment — paste it and the challenge is signed for real. It is kept in memory for that process only: never written to disk, never logged, gone on restart. A wrong key produces a genuine signature that genuinely fails.

## Your data

Your own eVault records, grouped by the domain each schema declares. That grouping is what a certificate is written against, so it is also what decides who sees what.

The table shows every certified platform against every kind of data you hold, decided by the real certificate's domains and your real signed terms, using the same `authorize` an eVault would call. A platform certified for `social`, `finance` and `media` is allowed those and refused everything else — with the reason spelled out. It cannot reach your messages or your files, and nothing it presents will change that.

## Permissions

Being certified for a kind of data is not permission to do anything with it. This tab is where that is settled.

The domain list is the whole published vocabulary, not just what a platform was certified for — the domains it has no business with are listed too, marked as such, because asking for one and watching the certificate refuse it is the case worth seeing.

Grants are managed by the platform through `POST /api/grants`, not set by hand here — the page shows what happens under them. Each change writes an `AccessGrant` into the owner's eVault as a new revision; clearing both operations withdraws the grant rather than deleting it, so the record shows access was taken away rather than never given.

```bash
curl -X POST http://localhost:4310/api/grants \
-H 'Content-Type: application/json' \
-d '{"platformEname":"@…","domain":"social","operations":["read"]}'
```

**Deployment keys go in here, before you try anything.** Possession is the one link a reader cannot establish by looking, so whether the key is present decides what a check can even mean. Enter it and the deployment can answer a challenge for real; leave it out and every request stops at the handshake, which is the correct outcome.

**Try a request** then runs one all the way through — a named deployment, an operation, a domain — and reports which of the three gates decided.

A permitted read is not a verdict: it goes to the eVault and the records it returns are rendered underneath. A refused one fetches nothing, and says so — the eVault is never asked. A permitted write really writes, with text you supply, into a schema belonging to that domain, and then reads the domain back so you can see it landed.

Turn off write and a write is refused while a read still succeeds; withdraw the grant and the refusal changes from "has not been given permission" to "has been withdrawn".

## Your terms

The association says what a platform was found to be; you decide what that is worth. Set the minimum level and any domain refused outright. The reputation service is named in what you sign but is not a choice: there is one on the network today, so asking you to type its address would only be a way to get it wrong.

Signing goes to your wallet. The signing session id **is** the canonical payload of the statement, so what the wallet signs is exactly the digest of your terms — the signature then verifies against the statement on its own, without anyone trusting this app. The terms are published into your own eVault as an `Access Policy` record, world-readable, and the signature is checked again before the write.

Your terms can only narrow a certificate, never widen it. Permitting `finance` does not let a platform reach finance data it was not certified for.

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication)
- [Access Policy](/docs/W3DS%20Basics/Access-Policy)
116 changes: 116 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
---
sidebar_position: 10
---

# Authenticating your platform

Your deployment proves which release it is running, and the eVault decides what that release may touch. This page is the integration.

For the mechanism itself see [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication).

## Install

```bash
pnpm add @metastate-foundation/auth
```

Both halves ship in one package. Deployments import the signer, verifiers import the verifier; nothing stops you doing both, which is what the demonstrator does.

## What your deployment needs

GitW3 produces all of it when you deploy a release. None of it is secret except the private key, which never leaves your process.

```ts
import type { DeploymentIdentity } from "@metastate-foundation/auth/platform";

const identity: DeploymentIdentity = {
privateKey: process.env.DEPLOYMENT_PRIVATE_KEY!, // PKCS#8, base64
evidence: {
deploymentEname, deploymentName, environment,
deployerEname, platformEname, versionEname,
version, releaseTag, commitSha, publicKey,
deploymentKeyDocument, // binding document, bundle-signed
softwareVersionDocument, // binding document, same signature
accreditationJws, // the association's certificate
issuerJwksUri,
submissionProof, // the release proof the association reviewed
},
};
```

Store the private key the way you store any other deployment secret. If it leaks, the holder can authenticate as your deployment until the deployer revokes the key — it is the whole of the possession proof.

## Authenticating

```ts
import { authenticate } from "@metastate-foundation/auth/platform";

const result = await authenticate(identity, "https://vault.example");
```

That fetches a challenge, signs it, and posts the answer. If you want the two steps yourself — to add retries, or to talk to something other than HTTP — use `answerChallenge(identity, challenge)` and send the response however you like.

## Verifying, if you are the eVault

```ts
import {
createChallengeStore,
verifyHandshake,
authorize,
} from "@metastate-foundation/auth/platform";

const challenges = createChallengeStore(); // module scope, not per request

// POST /pp-auth/challenge
const challenge = challenges.issue(ownerEname);

// POST /pp-auth/verify
const chain = await verifyHandshake(response, {
audience: ownerEname,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL!,
store: challenges,
});

if (!chain.ok) {
// chain.links carries all six with a plain-English detail on each.
return refuse(chain.links.find((link) => !link.ok));
}
```

`chain.claim` is what you learned: platform, deployment, version, level, and the domains it may use.

Then the owner's terms, for each record touched:

```ts
const decision = authorize(policy, {
claim: chain.claim,
domain: schema.domain, // the domain the record's ontology declares
reputation: score ? { engine, score } : null,
});

if (!decision.allowed) return refuse(decision.reason);
```

`decision.reason` is written to be shown to a person. `decision.code` is for your logs.

Hold the challenge store at module scope. Issuing from one instance and redeeming in another rejects every legitimate handshake, and under Vite's dev server a module evaluated twice will do exactly that.

## Injection points

Three things are injectable, all defaulting to the ordinary behaviour:

- `verifyWalletSignature` — how a wallet signature is checked. Defaults to `signature-validator` against your registry.
- `resolveJwks` — how a JWKS URI becomes keys. Defaults to a cached remote fetch. Supply your own to pin a key set or to run offline.
- `now` — the clock, for testing time-dependent behaviour.

## Testing your integration

`@metastate-foundation/auth/platform/scenario` mints a complete, self-consistent chain from keys it generates, so you can exercise your verifier without a wallet, a registry or a live association:

```ts
import { createTrustRoots, mintDeployment } from "@metastate-foundation/auth/platform/scenario";
```

Everything it produces is genuinely signed and genuinely verified. What differs is the root: the keys standing in for the deployer, the registry and the association are local. **Never configure a production verifier with roots from this module** — a chain that verifies against them proves your code works, not that a platform is trustworthy.

The [demonstrator](/docs/Post%20Platform%20Guide/pp-auth-demonstrator) is built on it and is the fastest way to see the whole thing move.
68 changes: 68 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
sidebar_position: 6
---

# Access Policy

Certification tells you what a platform was found to be. It does not tell you whether you want to deal with it. That is the eVault owner's decision, and an **access policy** is where they write it down.

It is a signed statement rather than a stored setting, so it travels with the owner and anyone can check it — the eVault enforcing it, a platform working out whether it is even worth asking, or the owner auditing what they agreed to months later.

## What an owner sets

| Term | Meaning |
|---|---|
| `minimumLevel` | The weakest certification level they will deal with. A platform certified below it is refused whatever its certificate grants. |
| `reputationEngine` | Whose reputation scores they accept, as an eName or host. Blank means reputation is not consulted at all. Today the network runs one service, so applications may reasonably fix this rather than ask. |
| `minimumReputation` | The score that engine must report for the platform. Null means no threshold, which is the common case. |
| `allowedDomains` | Null means "whatever the certificate grants" — the ordinary case. A list narrows it further. |
| `deniedDomains` | Refused outright, overriding both the certificate and the allow list. |

Naming the engine matters. A score is only meaningful relative to how it was calculated, so the owner elects which calculation they accept rather than inheriting whichever engine a platform happens to cite. A score from an engine the owner did not name counts as no score at all.

## A policy can only narrow

An owner permitting `finance` does not let a social platform reach finance data. The certificate gate runs first and independently: if `finance` is not in what the association granted the release, nothing in the owner's policy can put it there.

This ordering is the point. The owner's terms are a second lock, not a master key.

## The statement

```json
{
"subject": "@849c0221-6f3f-55f9-95f0-f3b0d2b3092f",
"minimumLevel": "L3",
"reputationEngine": "@ereputation.w3ds",
"minimumReputation": 40,
"allowedDomains": null,
"deniedDomains": ["health"],
"issuedAt": "2026-08-30T16:04:11.230Z",
"nonce": "0f1c…"
}
```

Signed by the owner's wallet over `w3ds:access-policy:v1:` + base64url(sha256(canonical statement)). The signer must be the subject: a policy signed by anyone else is somebody setting terms on a vault that is not theirs, and is rejected.

The newest statement for a subject is the one in force. An owner who has never set one is treated as requiring **L2** — the lowest level the framework issues to a release whose responsible people are identified at all.

Published as the `Access Policy` ontology (`c7a41f6d-95b8-4e2a-9c33-8f0d1b6e4a72`), domain `governance`.

## Permissions are a separate question

A policy says which platforms you will deal with. It does not say what they may *do* — reading your posts is not the same as writing to them.

That is what an [`AccessGrant`](https://github.com/MetaState-Prototype-Project/prototype/blob/main/services/ontology/schemas/accessGrant.json) is for: a grantee, a resource, and permissions written as `resource:Action` (`social:Read`, `finance:Write`). Grants are **deny by default** — a platform that is certified for a domain and permitted by your policy still needs a grant covering the operation it is attempting.

Grants are append-only. Changing what a platform may do writes a new revision rather than editing the old record, and withdrawing access marks the grant revoked while keeping the permissions it used to carry. So "your access was withdrawn" and "you never had access" stay distinguishable, which matters when explaining a refusal to someone.

The three gates run in order, and each can only narrow the one before it:

1. **The certificate** — was this release assessed for this domain?
2. **Your policy** — will you deal with this platform at all?
3. **The grants** — may it do this particular thing?

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

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
2 changes: 1 addition & 1 deletion docs/docs/W3DS Basics/Links.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# Links
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
Show all changes
31 commits
Select commit Hold shift + click to select a range
86bb79a
feat: publish certification level, identity assurance and assessment …
coodos Aug 30, 2026
cdddb7c
feat: add the certification matrix and level computation
coodos Aug 30, 2026
77300ce
feat: derive identity assurance from binding documents
coodos Aug 30, 2026
ca1bfb8
feat: guide certification with the assessment matrix and derived iden…
coodos Aug 30, 2026
10eb8f6
feat: verify reputation from eReputation and move review into an asse…
coodos Aug 30, 2026
0909cc3
feat: score the assessment by geometric mean and hide the level override
coodos Aug 30, 2026
a9a4240
fix: stop a single L0 dimension collapsing the assessment score
coodos Aug 30, 2026
1522f8d
feat: state each assessment option as an answer a reviewer can choose
coodos Aug 30, 2026
5c15b24
fix: make every assessment option readable on its own
coodos Aug 30, 2026
6f0ee87
fix: word code review as work done, not findings implied
coodos Aug 30, 2026
8a54a36
fix: anchor the assessment drawer to the right edge
coodos Aug 30, 2026
008acee
feat: trace a deployment to its certificate with pp-auth
coodos Aug 30, 2026
07627a5
feat: let a vault owner sign the terms platforms must meet
coodos Aug 30, 2026
ed5acc6
feat: demonstrate domain separation across two deployments
coodos Aug 30, 2026
8f84cfc
docs: document platform authentication and owner terms
coodos Aug 30, 2026
96c8175
fix: canonicalise release statements in the order the wallet signed
coodos Aug 30, 2026
e1a892e
fix: show what the evidence supported when the identity floor caps it
coodos Aug 30, 2026
ff07f2f
feat: run the demonstrator on real platforms and your own eVault
coodos Aug 30, 2026
68b23f9
fix: point wallet callbacks at the address the page was served on
coodos Aug 30, 2026
ac17d50
feat: gate requests on what a platform may do, not just what it is
coodos Aug 30, 2026
55cb212
feat: return real records from a permitted read
coodos Aug 30, 2026
e9dedf3
feat: offer every published domain, and permit beside the request
coodos Aug 30, 2026
8451e0e
fix: drop the permission toggles from the request panel
coodos Aug 30, 2026
a875069
fix: record the demonstrator's dependencies in the lockfile
coodos Aug 30, 2026
294f5c3
feat: add platform management transfer
coodos Aug 30, 2026
0a7761e
feat: protect managed platform profiles
coodos Aug 30, 2026
a237a01
docs: document profile management
coodos Aug 30, 2026
77d0a2b
fix: drop the deployment assurance dimension
coodos Aug 30, 2026
db8562e
fix: describe the geometric mean, and reload the matrix when it changes
coodos Aug 30, 2026
32941b7
Merge pull request #1121 from MetaState-Prototype-Project/feat/platfo…
coodos Aug 30, 2026
337cf3e
Merge pull request #1119 from MetaState-Prototype-Project/feat/pp-aut…
coodos Aug 30, 2026
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
75 changes: 75 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
sidebar_position: 9
---

# PP Auth demonstrator

Shows platform authentication and domain separation against the live network: real platforms, real certificates from the association, real deployments, and your own eVault.

```bash
pnpm --filter pp-auth-demo dev
```

Then open **http://localhost:4310** and sign in with your wallet. It needs `PPA_AWARENESS_API_KEY` (or `AWARENESS_API_KEY`) to see the network, and `PUBLIC_REGISTRY_URL` to resolve eVaults.

Nothing is seeded. If the platforms page is empty, nothing has been deployed or certified yet — which is a true statement about the network rather than a failure of the app.

## Platforms

Every platform with a deployment or a certification decision, read live. Under each are the deployments actually running it, with the release and commit they were built from.

**Check it** verifies that deployment's chain of trust, from scratch, against records anyone can read:

| Link | Where the evidence comes from |
|---|---|
| Possession | the deployment itself — see below |
| Deployment authorised | the wallet signature on the deployment's key document, resolved through the registry |
| Bundle integrity | the hashes covered by that same signature |
| Version identity | UUIDv5 arithmetic over the platform eName and version |
| Release authorship | the release proof in the platform's own profile, and its registry key-binding certificate |
| Accreditation | the association's ES256 certificate for that exact version |

Five of the six are checked by reading. **Possession is not** — the deployment's private key never leaves the deployment, so a reader cannot answer a challenge on its behalf. That link reports "not attempted" rather than pretending it failed a check that was never made.

If you hold the key — because you are the person who made that deployment — paste it and the challenge is signed for real. It is kept in memory for that process only: never written to disk, never logged, gone on restart. A wrong key produces a genuine signature that genuinely fails.

## Your data

Your own eVault records, grouped by the domain each schema declares. That grouping is what a certificate is written against, so it is also what decides who sees what.

The table shows every certified platform against every kind of data you hold, decided by the real certificate's domains and your real signed terms, using the same `authorize` an eVault would call. A platform certified for `social`, `finance` and `media` is allowed those and refused everything else — with the reason spelled out. It cannot reach your messages or your files, and nothing it presents will change that.

## Permissions

Being certified for a kind of data is not permission to do anything with it. This tab is where that is settled.

The domain list is the whole published vocabulary, not just what a platform was certified for — the domains it has no business with are listed too, marked as such, because asking for one and watching the certificate refuse it is the case worth seeing.

Grants are managed by the platform through `POST /api/grants`, not set by hand here — the page shows what happens under them. Each change writes an `AccessGrant` into the owner's eVault as a new revision; clearing both operations withdraws the grant rather than deleting it, so the record shows access was taken away rather than never given.

```bash
curl -X POST http://localhost:4310/api/grants \
-H 'Content-Type: application/json' \
-d '{"platformEname":"@…","domain":"social","operations":["read"]}'
```

**Deployment keys go in here, before you try anything.** Possession is the one link a reader cannot establish by looking, so whether the key is present decides what a check can even mean. Enter it and the deployment can answer a challenge for real; leave it out and every request stops at the handshake, which is the correct outcome.

**Try a request** then runs one all the way through — a named deployment, an operation, a domain — and reports which of the three gates decided.

A permitted read is not a verdict: it goes to the eVault and the records it returns are rendered underneath. A refused one fetches nothing, and says so — the eVault is never asked. A permitted write really writes, with text you supply, into a schema belonging to that domain, and then reads the domain back so you can see it landed.

Turn off write and a write is refused while a read still succeeds; withdraw the grant and the refusal changes from "has not been given permission" to "has been withdrawn".

## Your terms

The association says what a platform was found to be; you decide what that is worth. Set the minimum level and any domain refused outright. The reputation service is named in what you sign but is not a choice: there is one on the network today, so asking you to type its address would only be a way to get it wrong.

Signing goes to your wallet. The signing session id **is** the canonical payload of the statement, so what the wallet signs is exactly the digest of your terms — the signature then verifies against the statement on its own, without anyone trusting this app. The terms are published into your own eVault as an `Access Policy` record, world-readable, and the signature is checked again before the write.

Your terms can only narrow a certificate, never widen it. Permitting `finance` does not let a platform reach finance data it was not certified for.

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication)
- [Access Policy](/docs/W3DS%20Basics/Access-Policy)
116 changes: 116 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
---
sidebar_position: 10
---

# Authenticating your platform

Your deployment proves which release it is running, and the eVault decides what that release may touch. This page is the integration.

For the mechanism itself see [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication).

## Install

```bash
pnpm add @metastate-foundation/auth
```

Both halves ship in one package. Deployments import the signer, verifiers import the verifier; nothing stops you doing both, which is what the demonstrator does.

## What your deployment needs

GitW3 produces all of it when you deploy a release. None of it is secret except the private key, which never leaves your process.

```ts
import type { DeploymentIdentity } from "@metastate-foundation/auth/platform";

const identity: DeploymentIdentity = {
privateKey: process.env.DEPLOYMENT_PRIVATE_KEY!, // PKCS#8, base64
evidence: {
deploymentEname, deploymentName, environment,
deployerEname, platformEname, versionEname,
version, releaseTag, commitSha, publicKey,
deploymentKeyDocument, // binding document, bundle-signed
softwareVersionDocument, // binding document, same signature
accreditationJws, // the association's certificate
issuerJwksUri,
submissionProof, // the release proof the association reviewed
},
};
```

Store the private key the way you store any other deployment secret. If it leaks, the holder can authenticate as your deployment until the deployer revokes the key — it is the whole of the possession proof.

## Authenticating

```ts
import { authenticate } from "@metastate-foundation/auth/platform";

const result = await authenticate(identity, "https://vault.example");
```

That fetches a challenge, signs it, and posts the answer. If you want the two steps yourself — to add retries, or to talk to something other than HTTP — use `answerChallenge(identity, challenge)` and send the response however you like.

## Verifying, if you are the eVault

```ts
import {
createChallengeStore,
verifyHandshake,
authorize,
} from "@metastate-foundation/auth/platform";

const challenges = createChallengeStore(); // module scope, not per request

// POST /pp-auth/challenge
const challenge = challenges.issue(ownerEname);

// POST /pp-auth/verify
const chain = await verifyHandshake(response, {
audience: ownerEname,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL!,
store: challenges,
});

if (!chain.ok) {
// chain.links carries all six with a plain-English detail on each.
return refuse(chain.links.find((link) => !link.ok));
}
```

`chain.claim` is what you learned: platform, deployment, version, level, and the domains it may use.

Then the owner's terms, for each record touched:

```ts
const decision = authorize(policy, {
claim: chain.claim,
domain: schema.domain, // the domain the record's ontology declares
reputation: score ? { engine, score } : null,
});

if (!decision.allowed) return refuse(decision.reason);
```

`decision.reason` is written to be shown to a person. `decision.code` is for your logs.

Hold the challenge store at module scope. Issuing from one instance and redeeming in another rejects every legitimate handshake, and under Vite's dev server a module evaluated twice will do exactly that.

## Injection points

Three things are injectable, all defaulting to the ordinary behaviour:

- `verifyWalletSignature` — how a wallet signature is checked. Defaults to `signature-validator` against your registry.
- `resolveJwks` — how a JWKS URI becomes keys. Defaults to a cached remote fetch. Supply your own to pin a key set or to run offline.
- `now` — the clock, for testing time-dependent behaviour.

## Testing your integration

`@metastate-foundation/auth/platform/scenario` mints a complete, self-consistent chain from keys it generates, so you can exercise your verifier without a wallet, a registry or a live association:

```ts
import { createTrustRoots, mintDeployment } from "@metastate-foundation/auth/platform/scenario";
```

Everything it produces is genuinely signed and genuinely verified. What differs is the root: the keys standing in for the deployer, the registry and the association are local. **Never configure a production verifier with roots from this module** — a chain that verifies against them proves your code works, not that a platform is trustworthy.

The [demonstrator](/docs/Post%20Platform%20Guide/pp-auth-demonstrator) is built on it and is the fastest way to see the whole thing move.
68 changes: 68 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
sidebar_position: 6
---

# Access Policy

Certification tells you what a platform was found to be. It does not tell you whether you want to deal with it. That is the eVault owner's decision, and an **access policy** is where they write it down.

It is a signed statement rather than a stored setting, so it travels with the owner and anyone can check it — the eVault enforcing it, a platform working out whether it is even worth asking, or the owner auditing what they agreed to months later.

## What an owner sets

| Term | Meaning |
|---|---|
| `minimumLevel` | The weakest certification level they will deal with. A platform certified below it is refused whatever its certificate grants. |
| `reputationEngine` | Whose reputation scores they accept, as an eName or host. Blank means reputation is not consulted at all. Today the network runs one service, so applications may reasonably fix this rather than ask. |
| `minimumReputation` | The score that engine must report for the platform. Null means no threshold, which is the common case. |
| `allowedDomains` | Null means "whatever the certificate grants" — the ordinary case. A list narrows it further. |
| `deniedDomains` | Refused outright, overriding both the certificate and the allow list. |

Naming the engine matters. A score is only meaningful relative to how it was calculated, so the owner elects which calculation they accept rather than inheriting whichever engine a platform happens to cite. A score from an engine the owner did not name counts as no score at all.

## A policy can only narrow

An owner permitting `finance` does not let a social platform reach finance data. The certificate gate runs first and independently: if `finance` is not in what the association granted the release, nothing in the owner's policy can put it there.

This ordering is the point. The owner's terms are a second lock, not a master key.

## The statement

```json
{
"subject": "@849c0221-6f3f-55f9-95f0-f3b0d2b3092f",
"minimumLevel": "L3",
"reputationEngine": "@ereputation.w3ds",
"minimumReputation": 40,
"allowedDomains": null,
"deniedDomains": ["health"],
"issuedAt": "2026-08-30T16:04:11.230Z",
"nonce": "0f1c…"
}
```

Signed by the owner's wallet over `w3ds:access-policy:v1:` + base64url(sha256(canonical statement)). The signer must be the subject: a policy signed by anyone else is somebody setting terms on a vault that is not theirs, and is rejected.

The newest statement for a subject is the one in force. An owner who has never set one is treated as requiring **L2** — the lowest level the framework issues to a release whose responsible people are identified at all.

Published as the `Access Policy` ontology (`c7a41f6d-95b8-4e2a-9c33-8f0d1b6e4a72`), domain `governance`.

## Permissions are a separate question

A policy says which platforms you will deal with. It does not say what they may *do* — reading your posts is not the same as writing to them.

That is what an [`AccessGrant`](https://github.com/MetaState-Prototype-Project/prototype/blob/main/services/ontology/schemas/accessGrant.json) is for: a grantee, a resource, and permissions written as `resource:Action` (`social:Read`, `finance:Write`). Grants are **deny by default** — a platform that is certified for a domain and permitted by your policy still needs a grant covering the operation it is attempting.

Grants are append-only. Changing what a platform may do writes a new revision rather than editing the old record, and withdrawing access marks the grant revoked while keeping the permissions it used to carry. So "your access was withdrawn" and "you never had access" stay distinguishable, which matters when explaining a refusal to someone.

The three gates run in order, and each can only narrow the one before it:

1. **The certificate** — was this release assessed for this domain?
2. **Your policy** — will you deal with this platform at all?
3. **The grants** — may it do this particular thing?

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

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
2 changes: 1 addition & 1 deletion docs/docs/W3DS Basics/Links.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# Links
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
Show all changes
31 commits
Select commit Hold shift + click to select a range
86bb79a
feat: publish certification level, identity assurance and assessment …
coodos Aug 30, 2026
cdddb7c
feat: add the certification matrix and level computation
coodos Aug 30, 2026
77300ce
feat: derive identity assurance from binding documents
coodos Aug 30, 2026
ca1bfb8
feat: guide certification with the assessment matrix and derived iden…
coodos Aug 30, 2026
10eb8f6
feat: verify reputation from eReputation and move review into an asse…
coodos Aug 30, 2026
0909cc3
feat: score the assessment by geometric mean and hide the level override
coodos Aug 30, 2026
a9a4240
fix: stop a single L0 dimension collapsing the assessment score
coodos Aug 30, 2026
1522f8d
feat: state each assessment option as an answer a reviewer can choose
coodos Aug 30, 2026
5c15b24
fix: make every assessment option readable on its own
coodos Aug 30, 2026
6f0ee87
fix: word code review as work done, not findings implied
coodos Aug 30, 2026
8a54a36
fix: anchor the assessment drawer to the right edge
coodos Aug 30, 2026
008acee
feat: trace a deployment to its certificate with pp-auth
coodos Aug 30, 2026
07627a5
feat: let a vault owner sign the terms platforms must meet
coodos Aug 30, 2026
ed5acc6
feat: demonstrate domain separation across two deployments
coodos Aug 30, 2026
8f84cfc
docs: document platform authentication and owner terms
coodos Aug 30, 2026
96c8175
fix: canonicalise release statements in the order the wallet signed
coodos Aug 30, 2026
e1a892e
fix: show what the evidence supported when the identity floor caps it
coodos Aug 30, 2026
ff07f2f
feat: run the demonstrator on real platforms and your own eVault
coodos Aug 30, 2026
68b23f9
fix: point wallet callbacks at the address the page was served on
coodos Aug 30, 2026
ac17d50
feat: gate requests on what a platform may do, not just what it is
coodos Aug 30, 2026
55cb212
feat: return real records from a permitted read
coodos Aug 30, 2026
e9dedf3
feat: offer every published domain, and permit beside the request
coodos Aug 30, 2026
8451e0e
fix: drop the permission toggles from the request panel
coodos Aug 30, 2026
a875069
fix: record the demonstrator's dependencies in the lockfile
coodos Aug 30, 2026
294f5c3
feat: add platform management transfer
coodos Aug 30, 2026
0a7761e
feat: protect managed platform profiles
coodos Aug 30, 2026
a237a01
docs: document profile management
coodos Aug 30, 2026
77d0a2b
fix: drop the deployment assurance dimension
coodos Aug 30, 2026
db8562e
fix: describe the geometric mean, and reload the matrix when it changes
coodos Aug 30, 2026
32941b7
Merge pull request #1121 from MetaState-Prototype-Project/feat/platfo…
coodos Aug 30, 2026
337cf3e
Merge pull request #1119 from MetaState-Prototype-Project/feat/pp-aut…
coodos Aug 30, 2026
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
75 changes: 75 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
sidebar_position: 9
---

# PP Auth demonstrator

Shows platform authentication and domain separation against the live network: real platforms, real certificates from the association, real deployments, and your own eVault.

```bash
pnpm --filter pp-auth-demo dev
```

Then open **http://localhost:4310** and sign in with your wallet. It needs `PPA_AWARENESS_API_KEY` (or `AWARENESS_API_KEY`) to see the network, and `PUBLIC_REGISTRY_URL` to resolve eVaults.

Nothing is seeded. If the platforms page is empty, nothing has been deployed or certified yet — which is a true statement about the network rather than a failure of the app.

## Platforms

Every platform with a deployment or a certification decision, read live. Under each are the deployments actually running it, with the release and commit they were built from.

**Check it** verifies that deployment's chain of trust, from scratch, against records anyone can read:

| Link | Where the evidence comes from |
|---|---|
| Possession | the deployment itself — see below |
| Deployment authorised | the wallet signature on the deployment's key document, resolved through the registry |
| Bundle integrity | the hashes covered by that same signature |
| Version identity | UUIDv5 arithmetic over the platform eName and version |
| Release authorship | the release proof in the platform's own profile, and its registry key-binding certificate |
| Accreditation | the association's ES256 certificate for that exact version |

Five of the six are checked by reading. **Possession is not** — the deployment's private key never leaves the deployment, so a reader cannot answer a challenge on its behalf. That link reports "not attempted" rather than pretending it failed a check that was never made.

If you hold the key — because you are the person who made that deployment — paste it and the challenge is signed for real. It is kept in memory for that process only: never written to disk, never logged, gone on restart. A wrong key produces a genuine signature that genuinely fails.

## Your data

Your own eVault records, grouped by the domain each schema declares. That grouping is what a certificate is written against, so it is also what decides who sees what.

The table shows every certified platform against every kind of data you hold, decided by the real certificate's domains and your real signed terms, using the same `authorize` an eVault would call. A platform certified for `social`, `finance` and `media` is allowed those and refused everything else — with the reason spelled out. It cannot reach your messages or your files, and nothing it presents will change that.

## Permissions

Being certified for a kind of data is not permission to do anything with it. This tab is where that is settled.

The domain list is the whole published vocabulary, not just what a platform was certified for — the domains it has no business with are listed too, marked as such, because asking for one and watching the certificate refuse it is the case worth seeing.

Grants are managed by the platform through `POST /api/grants`, not set by hand here — the page shows what happens under them. Each change writes an `AccessGrant` into the owner's eVault as a new revision; clearing both operations withdraws the grant rather than deleting it, so the record shows access was taken away rather than never given.

```bash
curl -X POST http://localhost:4310/api/grants \
-H 'Content-Type: application/json' \
-d '{"platformEname":"@…","domain":"social","operations":["read"]}'
```

**Deployment keys go in here, before you try anything.** Possession is the one link a reader cannot establish by looking, so whether the key is present decides what a check can even mean. Enter it and the deployment can answer a challenge for real; leave it out and every request stops at the handshake, which is the correct outcome.

**Try a request** then runs one all the way through — a named deployment, an operation, a domain — and reports which of the three gates decided.

A permitted read is not a verdict: it goes to the eVault and the records it returns are rendered underneath. A refused one fetches nothing, and says so — the eVault is never asked. A permitted write really writes, with text you supply, into a schema belonging to that domain, and then reads the domain back so you can see it landed.

Turn off write and a write is refused while a read still succeeds; withdraw the grant and the refusal changes from "has not been given permission" to "has been withdrawn".

## Your terms

The association says what a platform was found to be; you decide what that is worth. Set the minimum level and any domain refused outright. The reputation service is named in what you sign but is not a choice: there is one on the network today, so asking you to type its address would only be a way to get it wrong.

Signing goes to your wallet. The signing session id **is** the canonical payload of the statement, so what the wallet signs is exactly the digest of your terms — the signature then verifies against the statement on its own, without anyone trusting this app. The terms are published into your own eVault as an `Access Policy` record, world-readable, and the signature is checked again before the write.

Your terms can only narrow a certificate, never widen it. Permitting `finance` does not let a platform reach finance data it was not certified for.

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication)
- [Access Policy](/docs/W3DS%20Basics/Access-Policy)
116 changes: 116 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
---
sidebar_position: 10
---

# Authenticating your platform

Your deployment proves which release it is running, and the eVault decides what that release may touch. This page is the integration.

For the mechanism itself see [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication).

## Install

```bash
pnpm add @metastate-foundation/auth
```

Both halves ship in one package. Deployments import the signer, verifiers import the verifier; nothing stops you doing both, which is what the demonstrator does.

## What your deployment needs

GitW3 produces all of it when you deploy a release. None of it is secret except the private key, which never leaves your process.

```ts
import type { DeploymentIdentity } from "@metastate-foundation/auth/platform";

const identity: DeploymentIdentity = {
privateKey: process.env.DEPLOYMENT_PRIVATE_KEY!, // PKCS#8, base64
evidence: {
deploymentEname, deploymentName, environment,
deployerEname, platformEname, versionEname,
version, releaseTag, commitSha, publicKey,
deploymentKeyDocument, // binding document, bundle-signed
softwareVersionDocument, // binding document, same signature
accreditationJws, // the association's certificate
issuerJwksUri,
submissionProof, // the release proof the association reviewed
},
};
```

Store the private key the way you store any other deployment secret. If it leaks, the holder can authenticate as your deployment until the deployer revokes the key — it is the whole of the possession proof.

## Authenticating

```ts
import { authenticate } from "@metastate-foundation/auth/platform";

const result = await authenticate(identity, "https://vault.example");
```

That fetches a challenge, signs it, and posts the answer. If you want the two steps yourself — to add retries, or to talk to something other than HTTP — use `answerChallenge(identity, challenge)` and send the response however you like.

## Verifying, if you are the eVault

```ts
import {
createChallengeStore,
verifyHandshake,
authorize,
} from "@metastate-foundation/auth/platform";

const challenges = createChallengeStore(); // module scope, not per request

// POST /pp-auth/challenge
const challenge = challenges.issue(ownerEname);

// POST /pp-auth/verify
const chain = await verifyHandshake(response, {
audience: ownerEname,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL!,
store: challenges,
});

if (!chain.ok) {
// chain.links carries all six with a plain-English detail on each.
return refuse(chain.links.find((link) => !link.ok));
}
```

`chain.claim` is what you learned: platform, deployment, version, level, and the domains it may use.

Then the owner's terms, for each record touched:

```ts
const decision = authorize(policy, {
claim: chain.claim,
domain: schema.domain, // the domain the record's ontology declares
reputation: score ? { engine, score } : null,
});

if (!decision.allowed) return refuse(decision.reason);
```

`decision.reason` is written to be shown to a person. `decision.code` is for your logs.

Hold the challenge store at module scope. Issuing from one instance and redeeming in another rejects every legitimate handshake, and under Vite's dev server a module evaluated twice will do exactly that.

## Injection points

Three things are injectable, all defaulting to the ordinary behaviour:

- `verifyWalletSignature` — how a wallet signature is checked. Defaults to `signature-validator` against your registry.
- `resolveJwks` — how a JWKS URI becomes keys. Defaults to a cached remote fetch. Supply your own to pin a key set or to run offline.
- `now` — the clock, for testing time-dependent behaviour.

## Testing your integration

`@metastate-foundation/auth/platform/scenario` mints a complete, self-consistent chain from keys it generates, so you can exercise your verifier without a wallet, a registry or a live association:

```ts
import { createTrustRoots, mintDeployment } from "@metastate-foundation/auth/platform/scenario";
```

Everything it produces is genuinely signed and genuinely verified. What differs is the root: the keys standing in for the deployer, the registry and the association are local. **Never configure a production verifier with roots from this module** — a chain that verifies against them proves your code works, not that a platform is trustworthy.

The [demonstrator](/docs/Post%20Platform%20Guide/pp-auth-demonstrator) is built on it and is the fastest way to see the whole thing move.
68 changes: 68 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
sidebar_position: 6
---

# Access Policy

Certification tells you what a platform was found to be. It does not tell you whether you want to deal with it. That is the eVault owner's decision, and an **access policy** is where they write it down.

It is a signed statement rather than a stored setting, so it travels with the owner and anyone can check it — the eVault enforcing it, a platform working out whether it is even worth asking, or the owner auditing what they agreed to months later.

## What an owner sets

| Term | Meaning |
|---|---|
| `minimumLevel` | The weakest certification level they will deal with. A platform certified below it is refused whatever its certificate grants. |
| `reputationEngine` | Whose reputation scores they accept, as an eName or host. Blank means reputation is not consulted at all. Today the network runs one service, so applications may reasonably fix this rather than ask. |
| `minimumReputation` | The score that engine must report for the platform. Null means no threshold, which is the common case. |
| `allowedDomains` | Null means "whatever the certificate grants" — the ordinary case. A list narrows it further. |
| `deniedDomains` | Refused outright, overriding both the certificate and the allow list. |

Naming the engine matters. A score is only meaningful relative to how it was calculated, so the owner elects which calculation they accept rather than inheriting whichever engine a platform happens to cite. A score from an engine the owner did not name counts as no score at all.

## A policy can only narrow

An owner permitting `finance` does not let a social platform reach finance data. The certificate gate runs first and independently: if `finance` is not in what the association granted the release, nothing in the owner's policy can put it there.

This ordering is the point. The owner's terms are a second lock, not a master key.

## The statement

```json
{
"subject": "@849c0221-6f3f-55f9-95f0-f3b0d2b3092f",
"minimumLevel": "L3",
"reputationEngine": "@ereputation.w3ds",
"minimumReputation": 40,
"allowedDomains": null,
"deniedDomains": ["health"],
"issuedAt": "2026-08-30T16:04:11.230Z",
"nonce": "0f1c…"
}
```

Signed by the owner's wallet over `w3ds:access-policy:v1:` + base64url(sha256(canonical statement)). The signer must be the subject: a policy signed by anyone else is somebody setting terms on a vault that is not theirs, and is rejected.

The newest statement for a subject is the one in force. An owner who has never set one is treated as requiring **L2** — the lowest level the framework issues to a release whose responsible people are identified at all.

Published as the `Access Policy` ontology (`c7a41f6d-95b8-4e2a-9c33-8f0d1b6e4a72`), domain `governance`.

## Permissions are a separate question

A policy says which platforms you will deal with. It does not say what they may *do* — reading your posts is not the same as writing to them.

That is what an [`AccessGrant`](https://github.com/MetaState-Prototype-Project/prototype/blob/main/services/ontology/schemas/accessGrant.json) is for: a grantee, a resource, and permissions written as `resource:Action` (`social:Read`, `finance:Write`). Grants are **deny by default** — a platform that is certified for a domain and permitted by your policy still needs a grant covering the operation it is attempting.

Grants are append-only. Changing what a platform may do writes a new revision rather than editing the old record, and withdrawing access marks the grant revoked while keeping the permissions it used to carry. So "your access was withdrawn" and "you never had access" stay distinguishable, which matters when explaining a refusal to someone.

The three gates run in order, and each can only narrow the one before it:

1. **The certificate** — was this release assessed for this domain?
2. **Your policy** — will you deal with this platform at all?
3. **The grants** — may it do this particular thing?

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

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
2 changes: 1 addition & 1 deletion docs/docs/W3DS Basics/Links.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# Links
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
Show all changes
31 commits
Select commit Hold shift + click to select a range
86bb79a
feat: publish certification level, identity assurance and assessment …
coodos Aug 30, 2026
cdddb7c
feat: add the certification matrix and level computation
coodos Aug 30, 2026
77300ce
feat: derive identity assurance from binding documents
coodos Aug 30, 2026
ca1bfb8
feat: guide certification with the assessment matrix and derived iden…
coodos Aug 30, 2026
10eb8f6
feat: verify reputation from eReputation and move review into an asse…
coodos Aug 30, 2026
0909cc3
feat: score the assessment by geometric mean and hide the level override
coodos Aug 30, 2026
a9a4240
fix: stop a single L0 dimension collapsing the assessment score
coodos Aug 30, 2026
1522f8d
feat: state each assessment option as an answer a reviewer can choose
coodos Aug 30, 2026
5c15b24
fix: make every assessment option readable on its own
coodos Aug 30, 2026
6f0ee87
fix: word code review as work done, not findings implied
coodos Aug 30, 2026
8a54a36
fix: anchor the assessment drawer to the right edge
coodos Aug 30, 2026
008acee
feat: trace a deployment to its certificate with pp-auth
coodos Aug 30, 2026
07627a5
feat: let a vault owner sign the terms platforms must meet
coodos Aug 30, 2026
ed5acc6
feat: demonstrate domain separation across two deployments
coodos Aug 30, 2026
8f84cfc
docs: document platform authentication and owner terms
coodos Aug 30, 2026
96c8175
fix: canonicalise release statements in the order the wallet signed
coodos Aug 30, 2026
e1a892e
fix: show what the evidence supported when the identity floor caps it
coodos Aug 30, 2026
ff07f2f
feat: run the demonstrator on real platforms and your own eVault
coodos Aug 30, 2026
68b23f9
fix: point wallet callbacks at the address the page was served on
coodos Aug 30, 2026
ac17d50
feat: gate requests on what a platform may do, not just what it is
coodos Aug 30, 2026
55cb212
feat: return real records from a permitted read
coodos Aug 30, 2026
e9dedf3
feat: offer every published domain, and permit beside the request
coodos Aug 30, 2026
8451e0e
fix: drop the permission toggles from the request panel
coodos Aug 30, 2026
a875069
fix: record the demonstrator's dependencies in the lockfile
coodos Aug 30, 2026
294f5c3
feat: add platform management transfer
coodos Aug 30, 2026
0a7761e
feat: protect managed platform profiles
coodos Aug 30, 2026
a237a01
docs: document profile management
coodos Aug 30, 2026
77d0a2b
fix: drop the deployment assurance dimension
coodos Aug 30, 2026
db8562e
fix: describe the geometric mean, and reload the matrix when it changes
coodos Aug 30, 2026
32941b7
Merge pull request #1121 from MetaState-Prototype-Project/feat/platfo…
coodos Aug 30, 2026
337cf3e
Merge pull request #1119 from MetaState-Prototype-Project/feat/pp-aut…
coodos Aug 30, 2026
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
75 changes: 75 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth-demonstrator.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
sidebar_position: 9
---

# PP Auth demonstrator

Shows platform authentication and domain separation against the live network: real platforms, real certificates from the association, real deployments, and your own eVault.

```bash
pnpm --filter pp-auth-demo dev
```

Then open **http://localhost:4310** and sign in with your wallet. It needs `PPA_AWARENESS_API_KEY` (or `AWARENESS_API_KEY`) to see the network, and `PUBLIC_REGISTRY_URL` to resolve eVaults.

Nothing is seeded. If the platforms page is empty, nothing has been deployed or certified yet — which is a true statement about the network rather than a failure of the app.

## Platforms

Every platform with a deployment or a certification decision, read live. Under each are the deployments actually running it, with the release and commit they were built from.

**Check it** verifies that deployment's chain of trust, from scratch, against records anyone can read:

| Link | Where the evidence comes from |
|---|---|
| Possession | the deployment itself — see below |
| Deployment authorised | the wallet signature on the deployment's key document, resolved through the registry |
| Bundle integrity | the hashes covered by that same signature |
| Version identity | UUIDv5 arithmetic over the platform eName and version |
| Release authorship | the release proof in the platform's own profile, and its registry key-binding certificate |
| Accreditation | the association's ES256 certificate for that exact version |

Five of the six are checked by reading. **Possession is not** — the deployment's private key never leaves the deployment, so a reader cannot answer a challenge on its behalf. That link reports "not attempted" rather than pretending it failed a check that was never made.

If you hold the key — because you are the person who made that deployment — paste it and the challenge is signed for real. It is kept in memory for that process only: never written to disk, never logged, gone on restart. A wrong key produces a genuine signature that genuinely fails.

## Your data

Your own eVault records, grouped by the domain each schema declares. That grouping is what a certificate is written against, so it is also what decides who sees what.

The table shows every certified platform against every kind of data you hold, decided by the real certificate's domains and your real signed terms, using the same `authorize` an eVault would call. A platform certified for `social`, `finance` and `media` is allowed those and refused everything else — with the reason spelled out. It cannot reach your messages or your files, and nothing it presents will change that.

## Permissions

Being certified for a kind of data is not permission to do anything with it. This tab is where that is settled.

The domain list is the whole published vocabulary, not just what a platform was certified for — the domains it has no business with are listed too, marked as such, because asking for one and watching the certificate refuse it is the case worth seeing.

Grants are managed by the platform through `POST /api/grants`, not set by hand here — the page shows what happens under them. Each change writes an `AccessGrant` into the owner's eVault as a new revision; clearing both operations withdraws the grant rather than deleting it, so the record shows access was taken away rather than never given.

```bash
curl -X POST http://localhost:4310/api/grants \
-H 'Content-Type: application/json' \
-d '{"platformEname":"@…","domain":"social","operations":["read"]}'
```

**Deployment keys go in here, before you try anything.** Possession is the one link a reader cannot establish by looking, so whether the key is present decides what a check can even mean. Enter it and the deployment can answer a challenge for real; leave it out and every request stops at the handshake, which is the correct outcome.

**Try a request** then runs one all the way through — a named deployment, an operation, a domain — and reports which of the three gates decided.

A permitted read is not a verdict: it goes to the eVault and the records it returns are rendered underneath. A refused one fetches nothing, and says so — the eVault is never asked. A permitted write really writes, with text you supply, into a schema belonging to that domain, and then reads the domain back so you can see it landed.

Turn off write and a write is refused while a read still succeeds; withdraw the grant and the refusal changes from "has not been given permission" to "has been withdrawn".

## Your terms

The association says what a platform was found to be; you decide what that is worth. Set the minimum level and any domain refused outright. The reputation service is named in what you sign but is not a choice: there is one on the network today, so asking you to type its address would only be a way to get it wrong.

Signing goes to your wallet. The signing session id **is** the canonical payload of the statement, so what the wallet signs is exactly the digest of your terms — the signature then verifies against the statement on its own, without anyone trusting this app. The terms are published into your own eVault as an `Access Policy` record, world-readable, and the signature is checked again before the write.

Your terms can only narrow a certificate, never widen it. Permitting `finance` does not let a platform reach finance data it was not certified for.

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication)
- [Access Policy](/docs/W3DS%20Basics/Access-Policy)
116 changes: 116 additions & 0 deletions docs/docs/Post Platform Guide/pp-auth.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
---
sidebar_position: 10
---

# Authenticating your platform

Your deployment proves which release it is running, and the eVault decides what that release may touch. This page is the integration.

For the mechanism itself see [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication).

## Install

```bash
pnpm add @metastate-foundation/auth
```

Both halves ship in one package. Deployments import the signer, verifiers import the verifier; nothing stops you doing both, which is what the demonstrator does.

## What your deployment needs

GitW3 produces all of it when you deploy a release. None of it is secret except the private key, which never leaves your process.

```ts
import type { DeploymentIdentity } from "@metastate-foundation/auth/platform";

const identity: DeploymentIdentity = {
privateKey: process.env.DEPLOYMENT_PRIVATE_KEY!, // PKCS#8, base64
evidence: {
deploymentEname, deploymentName, environment,
deployerEname, platformEname, versionEname,
version, releaseTag, commitSha, publicKey,
deploymentKeyDocument, // binding document, bundle-signed
softwareVersionDocument, // binding document, same signature
accreditationJws, // the association's certificate
issuerJwksUri,
submissionProof, // the release proof the association reviewed
},
};
```

Store the private key the way you store any other deployment secret. If it leaks, the holder can authenticate as your deployment until the deployer revokes the key — it is the whole of the possession proof.

## Authenticating

```ts
import { authenticate } from "@metastate-foundation/auth/platform";

const result = await authenticate(identity, "https://vault.example");
```

That fetches a challenge, signs it, and posts the answer. If you want the two steps yourself — to add retries, or to talk to something other than HTTP — use `answerChallenge(identity, challenge)` and send the response however you like.

## Verifying, if you are the eVault

```ts
import {
createChallengeStore,
verifyHandshake,
authorize,
} from "@metastate-foundation/auth/platform";

const challenges = createChallengeStore(); // module scope, not per request

// POST /pp-auth/challenge
const challenge = challenges.issue(ownerEname);

// POST /pp-auth/verify
const chain = await verifyHandshake(response, {
audience: ownerEname,
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL!,
store: challenges,
});

if (!chain.ok) {
// chain.links carries all six with a plain-English detail on each.
return refuse(chain.links.find((link) => !link.ok));
}
```

`chain.claim` is what you learned: platform, deployment, version, level, and the domains it may use.

Then the owner's terms, for each record touched:

```ts
const decision = authorize(policy, {
claim: chain.claim,
domain: schema.domain, // the domain the record's ontology declares
reputation: score ? { engine, score } : null,
});

if (!decision.allowed) return refuse(decision.reason);
```

`decision.reason` is written to be shown to a person. `decision.code` is for your logs.

Hold the challenge store at module scope. Issuing from one instance and redeeming in another rejects every legitimate handshake, and under Vite's dev server a module evaluated twice will do exactly that.

## Injection points

Three things are injectable, all defaulting to the ordinary behaviour:

- `verifyWalletSignature` — how a wallet signature is checked. Defaults to `signature-validator` against your registry.
- `resolveJwks` — how a JWKS URI becomes keys. Defaults to a cached remote fetch. Supply your own to pin a key set or to run offline.
- `now` — the clock, for testing time-dependent behaviour.

## Testing your integration

`@metastate-foundation/auth/platform/scenario` mints a complete, self-consistent chain from keys it generates, so you can exercise your verifier without a wallet, a registry or a live association:

```ts
import { createTrustRoots, mintDeployment } from "@metastate-foundation/auth/platform/scenario";
```

Everything it produces is genuinely signed and genuinely verified. What differs is the root: the keys standing in for the deployer, the registry and the association are local. **Never configure a production verifier with roots from this module** — a chain that verifies against them proves your code works, not that a platform is trustworthy.

The [demonstrator](/docs/Post%20Platform%20Guide/pp-auth-demonstrator) is built on it and is the fastest way to see the whole thing move.
68 changes: 68 additions & 0 deletions docs/docs/W3DS Basics/Access-Policy.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
sidebar_position: 6
---

# Access Policy

Certification tells you what a platform was found to be. It does not tell you whether you want to deal with it. That is the eVault owner's decision, and an **access policy** is where they write it down.

It is a signed statement rather than a stored setting, so it travels with the owner and anyone can check it — the eVault enforcing it, a platform working out whether it is even worth asking, or the owner auditing what they agreed to months later.

## What an owner sets

| Term | Meaning |
|---|---|
| `minimumLevel` | The weakest certification level they will deal with. A platform certified below it is refused whatever its certificate grants. |
| `reputationEngine` | Whose reputation scores they accept, as an eName or host. Blank means reputation is not consulted at all. Today the network runs one service, so applications may reasonably fix this rather than ask. |
| `minimumReputation` | The score that engine must report for the platform. Null means no threshold, which is the common case. |
| `allowedDomains` | Null means "whatever the certificate grants" — the ordinary case. A list narrows it further. |
| `deniedDomains` | Refused outright, overriding both the certificate and the allow list. |

Naming the engine matters. A score is only meaningful relative to how it was calculated, so the owner elects which calculation they accept rather than inheriting whichever engine a platform happens to cite. A score from an engine the owner did not name counts as no score at all.

## A policy can only narrow

An owner permitting `finance` does not let a social platform reach finance data. The certificate gate runs first and independently: if `finance` is not in what the association granted the release, nothing in the owner's policy can put it there.

This ordering is the point. The owner's terms are a second lock, not a master key.

## The statement

```json
{
"subject": "@849c0221-6f3f-55f9-95f0-f3b0d2b3092f",
"minimumLevel": "L3",
"reputationEngine": "@ereputation.w3ds",
"minimumReputation": 40,
"allowedDomains": null,
"deniedDomains": ["health"],
"issuedAt": "2026-08-30T16:04:11.230Z",
"nonce": "0f1c…"
}
```

Signed by the owner's wallet over `w3ds:access-policy:v1:` + base64url(sha256(canonical statement)). The signer must be the subject: a policy signed by anyone else is somebody setting terms on a vault that is not theirs, and is rejected.

The newest statement for a subject is the one in force. An owner who has never set one is treated as requiring **L2** — the lowest level the framework issues to a release whose responsible people are identified at all.

Published as the `Access Policy` ontology (`c7a41f6d-95b8-4e2a-9c33-8f0d1b6e4a72`), domain `governance`.

## Permissions are a separate question

A policy says which platforms you will deal with. It does not say what they may *do* — reading your posts is not the same as writing to them.

That is what an [`AccessGrant`](https://github.com/MetaState-Prototype-Project/prototype/blob/main/services/ontology/schemas/accessGrant.json) is for: a grantee, a resource, and permissions written as `resource:Action` (`social:Read`, `finance:Write`). Grants are **deny by default** — a platform that is certified for a domain and permitted by your policy still needs a grant covering the operation it is attempting.

Grants are append-only. Changing what a platform may do writes a new revision rather than editing the old record, and withdrawing access marks the grant revoked while keeping the permissions it used to carry. So "your access was withdrawn" and "you never had access" stay distinguishable, which matters when explaining a refusal to someone.

The three gates run in order, and each can only narrow the one before it:

1. **The certificate** — was this release assessed for this domain?
2. **Your policy** — will you deal with this platform at all?
3. **The grants** — may it do this particular thing?

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

## See also

- [Platform Authentication](/docs/W3DS%20Protocol/Platform-Authentication) — how a platform proves which release it is running
2 changes: 1 addition & 1 deletion docs/docs/W3DS Basics/Links.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---

# Links
Expand Down
Loading
Loading