Unofficial Draft
Copyright © 2026 the document editors/authors. Text is available under the Creative Commons Attribution 4.0 International Public License; additional terms may apply.
Wallet Attached Storage is a general purpose permissioned storage API.
This document is a draft of a potential specification. It has no official standing of any kind and does not represent the support or consensus of any standards organization.
This is an experimental specification and is undergoing regular revisions.
As well as sections marked as non-normative, all authoring guidelines, diagrams, examples, and notes in this specification are non-normative. Everything else in this specification is normative.
The key words MAY, MUST, MUST NOT, OPTIONAL, RECOMMENDED, REQUIRED, SHOULD, and SHOULD NOT in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.
The Wallet Attached Storage (WAS) specification brings together the lessons learned from many attempts to standardize permissioned cloud storage over the years.
This specification aims to provide:
Authorization: ... is a placeholder. Every request example that carries an
Authorization header abbreviates a signed zCap (capability) invocation
(as opposed to a bearer token). Reads are authorized in WAS just as writes are.
The expanded form -- with the Digest, Capability-Invocation, and Signature
headers -- appears once, in 5.1.3 Performing Authorized API Calls.
A trailing slash means the container. A path ending in / addresses a
container: GET lists its members and POST adds a member to it. The same path
without the trailing slash addresses the item itself: GET returns that item's
description, PUT creates or replaces it, DELETE removes it. So
/space/{space_id}/{collection_id}/ is the Collection's member list, while
/space/{space_id}/{collection_id} is the Collection's description.
All examples share one Space. Every example in this document uses the Space
id 81246131-69a4-45ab-9bff-9c946b59cf2e on the host example.com. Path
segments in braces -- {space_id}, {collection_id}, {resource_id} -- are
placeholders for those identifiers.
Normative lists live in the appendices. Error type URIs are catalogued in
F. Error Type Registry, path segments this specification reserves in
B. Reserved Path Segment Registry, and client-side encryption schemes in
C. Encryption Scheme Registry. Those registries are normative: they, not the
surrounding prose, are where an implementation looks such values up.
Initial use cases that are motivating this work:
This specification represents a layered and modular approach to storage, combining core features and optional extension points.
The layers below describe conformance tiers. Each tier adds optional capability on top of the one before it, so an implementer can stop at any tier and still be conformant. The normative body, by contrast, is organized container-first (outermost to innermost: Spaces Repositories, Spaces, Collections, then Resources), which is convenient as a reference but is the reverse of the tiers. If you're new to the spec, scan the profile table below for the conformance tiers, then walk through 2.5 Quickstart: Your First Request to watch a request succeed. The core tier is just 11. Resources and Blobs plus 5.1 WAS Authorization Profile v0.1.
| Profile | Adds | Defining sections |
|---|---|---|
| Minimal | Resource CRUD (KV + blob read/write) + authorization | 11. Resources and Blobs, 5.1 WAS Authorization Profile v0.1 |
| + Listing | list resources / collections / spaces | 10.5 List Collection operation, 9.5 List All Collections operation, 8.2 List Spaces Operation |
| + Collection mgmt | create / manage collections in a Space | 10. Collections |
| + Space mgmt | manage an individual Space | 9.2 Read Space operation (Space endpoints) |
| + Multi-tenant | create / manage many Spaces on a server | 8. Spaces Repositories |
| + Extensions | linksets, policy, metadata, export, backends, query, quotas, encryption, replication, versioning | 13. Linksets |
Each tier stacks on the one above it. The Minimal profile is a permissioned key/value CRUD API built from simple HTTP verbs and delegatable capability-based authorization -- enough, on its own, to read and write any resource (text, structured document, or binary blob) without collection or space management.Listing, Collection and Space management will feel familiar to anyone who has used a GUI front end for a database or file system. Multi-tenant support lets a provider host many Spaces on one server. The Extensions tier layers on optional features -- an access-policy resource, user-writable metadata (for example, "tags" on binary files), a Space export endpoint, pluggable 14. Backends, query, quotas, client-side encryption (via Encrypted Data Vaults), replication, and versioning -- discovered through the linkset feature-detection mechanism (from [RFC9264]; see 13. Linksets).
Normative status. Section back-placement does not imply informative status. Everything from 2. Introduction through 15. Quotas is normative, as are the appendices A. Pagination, B. Reserved Path Segment Registry, C. Encryption Scheme Registry, and F. Error Type Registry (each of which also carries an inline "This appendix is normative." banner). The remaining appendices -- G. Goals and Requirements and H. IANA Considerations -- are informative. Optionality is orthogonal to normativity: many normative sections describe OPTIONAL endpoint groups, but a server that implements them MUST follow the stated requirements.
messages collection, and that you are that Space's controller. The goal is
to store one JSON Resource and read it back.
Step 1: Write. PUT a JSON document to a resource path under a collection.
A 204 No Content confirms the write:
PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...
{"message":"hi"}
HTTP/1.1 204 No Content
Step 2: Read. GET the same path to retrieve what you just stored:
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
HTTP/1.1 200 OK
Content-type: application/json
{"message":"hi"}
(The Authorization: ... placeholder stands for a signed zCap invocation).
WAS reads are authorized too, so the header is required on the
GET just as on the PUT. How that invocation is constructed is defined in
5.1.3 Performing Authorized API Calls, and its fully expanded form -- with the
Digest, Capability-Invocation, and Signature headers spelled out -- is
shown once in the worked example within that section. Producing that signature
requires a conformant WAS client: it is an Ed25519 did:key capability
invocation, not something you can hand-write with curl.
To go beyond a single pre-existing Space -- to create your own Space, or delegate access to others -- see the conformance-profile table in 2.4 Scope and Conformance Profiles and the full endpoint map in 2.6 API Summary.
API summary at a glance.
Unless otherwise specified by the controller, all operations
require authorization. This can be overridden by the controller via the
(optional) /policy endpoints. Hosting "public-read" resources, such as HTML
files for websites, or media files you can link to via <img src="">, is a
common use case.
The endpoints below divide into a Core profile that every conformant server implements, and Optional extensions grouped by feature. Each row links to the section that defines its operation. Rows marked Reserved name a path this specification anchors (see B. Reserved Path Segment Registry) but does not yet define an operation for; a server MUST NOT repurpose a reserved path.
Resource CRUD (Create, Read, Update, Delete):
POST /space/{space_id}/{collection_id}/ -- 11.4 Create Resource (Add Resource to Collection) OperationGET /space/{space_id}/{collection_id}/{resource_id} -- 11.5 Read Resource OperationPUT /space/{space_id}/{collection_id}/{resource_id} -- 11.6 Update (or Create By Id) Resource OperationDELETE /space/{space_id}/{collection_id}/{resource_id} -- 11.7 Delete Resource OperationHEAD instead of a GET to check a resource's headers/metadata without
fetching the whole resource. However, do not rely on this for atomic inserts
(that is, to check if a resource exists before attempting to create it).
Instead, use the backend-specific Transaction mechanisms when appropriate.List resources in a Collection:
If not implemented on a server, implies that only individual Key/Value operations are supported.
GET /space/{space_id}/{collection_id}/ -- 10.5 List Collection operationManage Collections in a Space:
If not implemented on a server, implies that collections are pre-configured or implicit, controlled by the server.
POST /space/{space_id}/collections/ -- 10.2 Create Collection (Add Collection to a Space) operationGET /space/{space_id}/collections/ -- 9.5 List All Collections operationGET /space/{space_id}/{collection_id} -- 10.4 Get Collection Description operationPUT /space/{space_id}/{collection_id} -- 10.3 Update (or Create By Id) Collection operationDELETE /space/{space_id}/{collection_id} -- 10.6 Delete Collection operationCollection Metadata Endpoints:
GET /space/{space_id}/{collection_id}/meta -- 10.8 Read Collection Metadata OperationPUT /space/{space_id}/{collection_id}/meta -- 10.9 Update Collection Metadata OperationSpaces Repository Endpoints -- Manage Spaces on a Server:
If not implemented on a server, implies that any existing Spaces are pre-configured and controlled by the server.
POST /spaces/ -- 8.1 Create Space operationGET /spaces/ -- 8.2 List Spaces OperationSpace Endpoints -- Manage an individual Space:
GET /space/{space_id} -- 9.2 Read Space operationPUT /space/{space_id} -- 9.3 Update (or Create by Id) Space operationDELETE /space/{space_id} -- 9.4 Delete Space operationPOST /space/{space_id}/export -- Reserved / not yet specified. Export
(download) a Space's contents (all collections and resources).POST /space/{space_id}/import -- Reserved / not yet specified. Import
(upload) a previously exported Space archive (the operation the registered
invalid-import error kind anticipates).Advanced Resource Endpoints:
GET /space/{space_id}/{collection_id}/{resource_id}/meta -- 11.9 Read Resource Metadata OperationPUT /space/{space_id}/{collection_id}/{resource_id}/meta -- 11.10 Update Resource Metadata OperationChunked Resource Endpoints (see 12. Chunked Resources; available only on a
backend advertising chunked-streams):
PUT|GET|HEAD|DELETE /space/{space_id}/{collection_id}/{resource_id}/chunks/{index}
-- store, read, head, delete a single chunk.GET /space/{space_id}/{collection_id}/{resource_id}/chunks/ -- list a
Resource's chunks.Policy Related Endpoints:
Policy overrides are hierarchical and inherited. A policy set for the entire Space applies to all its Collections and Resources (unless overridden by a more specific policy, either at the Collection or Resource level).
GET|PUT|DELETE /space/{space_id}/policy -- Reserved / not yet specified.
CRUD on the policy object for the Space.GET|PUT|DELETE /space/{space_id}/{collection_id}/policy -- Reserved / not
yet specified. CRUD on the policy object for the Collection.GET|PUT|DELETE /space/{space_id}/{collection_id}/{resource_id}/policy --
Reserved / not yet specified. CRUD on the policy object for the Resource.Linkset / Discovery Endpoints:
Required if Space endpoints or Collection endpoints are supported.
GET /space/{space_id}/linkset -- 13.1 Space LinksetGET /space/{space_id}/{collection_id}/linkset -- 13.2 Collection LinksetQuery Endpoints:
POST /space/{space_id}/query -- Cross-collection queries (backend-specific).POST /space/{space_id}/{collection_id}/query -- Queries within a Collection,
discriminated by a request-body profile; catalogued in D. Query Profile Registry.Backend Management Endpoints (see 14. Backends):
GET /space/{space_id}/backends -- 14.2 Space Backends AvailableGET /space/{space_id}/{collection_id}/backend -- 14.3 Collection Backend Selected
(the backend summary is also displayed in the Collection description object)Quota Endpoints (see 15. Quotas):
GET /space/{space_id}/quotas -- 15. Quotas: the Quota report object, grouped
by available Backend (add ?include=collections for a per-collection usage breakdown)GET /space/{space_id}/{collection_id}/quota -- 15. Quotas: the Quota report
object for the specific Collection (not all Backends will support per-collection
quotas however)GET, POST, PUT, DELETE) as its action
vocabulary. See section
5.1.5 Authorization Actions and the Root Capability.backend property, and is assigned
the default backend when it does not. See section
14. Backends.chunks
sub-path. The server stores a chunk exactly like a binary Resource
representation and never parses it; framing and reassembly are the client's
concern. Available only on a backend advertising the
chunked-streams feature. See section 12. Chunked Resources.type property that declares what
access a target grants to callers in general, independent of any zCap
a caller might present. A policy is stored at the /policy
auxiliary resource of a Space, Collection, or Resource. Policies are inherited
most-specific-wins (Resource over Collection over Space), can only broaden
access (never deny a caller holding a valid capability), and grant nothing
when absent or of an unrecognized type. See section
5.1.7 Access Control Policies./quotas auxiliary resource, and a
per-Collection breakdown from /quota. A write that would exceed
a quota is rejected with quota-exceeded (507); a single upload larger
than the backend's maxUploadBytes constraint is rejected with
payload-too-large (413). See section 15. Quotas.invocationTarget MUST match the request target for the invocation to be
valid. See section 5.1.5 Authorization Actions and the Root Capability.Space, Collection, and Resource identifiers used in this specification are required to have the following properties.
ids (denoted by {space_id} in URL templates) MUST be
unique within a given server, Collection ids (denoted by {collection_id}
in URL templates) MUST be unique within a given Space, and Resource ids
(denoted by {resource_id} in URL templates) MUST be unique within a given
Collection.Identifier length limits are currently left to the implementer. However, implementations SHOULD limit identifier length to their appropriate use case.
Identifier format constraints are currently left to the implementer. Common identifier formats include:
/posts/2020-01-01-hello-world).This specification uses [RFC9457] Problem Details for HTTP APIs for error responses.
application/problem+json
content type.type and title properties are REQUIRED.errors array with { detail, pointer } objects is encouraged where appropriate.The type property is a URI identifying the kind of problem (not the
operation), and the same type is reused across operations. See Appendix
F. Error Type Registry for the catalog of type URIs this specification
defines, along with their typical status codes and a canonical example response
for each kind.
When returning errors, keep in mind the principle of maximum privacy: always "not found" instead of "not authorized". The existence of a resource, collection, or space, is by itself an item of sensitive information. If a client makes an API call, and they have insufficient authorization to perform that action, a "not found" error (such as HTTP 404) MUST be returned, just as if that resource (or space or collection) did not exist. To put it another way, an unauthorized client (meaning, either not carrying any authorization in the request itself, or possessing insufficient permissions) MUST NOT be able to discover the existence of a resource based on the error response.
This privacy rule governs failures that would otherwise reveal whether a target exists. It does not require masking failures that describe the request itself:
Content-Type, or a capability invocation or
signature that is present but malformed or fails to verify -- MAY be reported
with precise status codes and types (for example invalid-request-body,
missing-content-type, invalid-authorization-header, or
controller-mismatch). These responses describe the request and do not
disclose whether any particular target exists."List Spaces" operations are an exception to 404 masking: rather than returning
an error, they return 200 OK with only the subset of items the caller is
authorized to see (an empty items array if none) -- see 8.2 List Spaces Operation.
This section defines HTTP mechanics that are shared across the operations in this specification, rather than belonging to any single endpoint: caching, conditional requests (optimistic concurrency control), and pagination of list responses.
WAS relies on ordinary [RFC9111] HTTP caching and defines no caching layer of
its own. On the read side, a Resource's strong ETag validator (see
7.2 Conditional Requests) drives standard validation: a GET carrying
If-None-Match: "<etag>" yields 304 Not Modified when the Resource is
unchanged. Servers SHOULD emit ETag (and MAY emit Last-Modified) on
GET/HEAD responses, and SHOULD mark responses to non-idempotent operations
as non-cacheable (for example, Cache-Control: no-store).
Cache-Control directive semantics beyond validation
are not yet specified.
Servers and backends MAY support [RFC9110] conditional requests to provide
optimistic concurrency control on writes. This mechanism helps prevent the
"lost update" problem, where two clients that both read version N of a Resource
each write version N+1 and the second silently clobbers the first. A backend
that supports this advertises the conditional-writes feature in its Backend
description (see 14.1 Backend Data Model); a client SHOULD use these
preconditions only against a backend that advertises support.
When supported, a Resource carries a strong ETag validator that changes
whenever its stored content changes. Servers SHOULD return the ETag on
GET/HEAD responses. The validator is opaque to clients: how a backend
derives it is a server-side concern (see 14.1 Backend Data Model).
A state-changing request (PUT or DELETE) MAY carry a precondition:
If-Match: "<etag>" -- perform the write only if the Resource's current
ETag matches it (an "update-if-unchanged"). If it does not match, the server
MUST NOT perform the write and MUST respond with precondition-failed
(412).If-None-Match: * -- perform the write only if the Resource does not yet
exist (a "create-if-absent"). If it already exists, the server MUST NOT
perform the write and MUST respond with precondition-failed (412).A server that supports conditional writes MUST evaluate the precondition
atomically with the write, so that two concurrent writers cannot both observe
the same prior version and both succeed; how a backend achieves this atomicity
is a server-side concern (see 14.1 Backend Data Model). A client recovers
from a 412 by re-reading the current Resource, re-applying its change on top
of the new version, and retrying.
As with id-conflict, a server MUST verify the caller's authorization
before evaluating a precondition, so a 412 is only ever observed by a caller
already authorized to write the target; an under-authorized caller receives the
merged not-found (404) instead, per the maximum-privacy rule in
6. Error Handling.
The mechanism extends to the Collection Description itself, whose update operation replaces the whole description -- so two concurrent recipient changes to a key-epoch roster (see C.4 Key Epochs) would otherwise silently clobber one another:
ETag header -- a
strong validator over the description's monotonic version (the same quoted
form Resources use).If-Match. A server advertising
the key-epochs feature MUST evaluate it atomically with the write and
reject a stale validator with a precondition-failed error (412). The
Update and Create responses carry the new ETag.If-Match is opt-in: an unconditional PUT remains valid (and remains
last-writer-wins). Recipient-management clients MUST use If-Match.The Collection Metadata object
carries a validator of its own on the same pattern, mutually independent of
both the Collection Description's and every Resource's: its metaVersion
changes only on Collection metadata writes, never on a Collection Description
update or a Resource write (and vice versa). Like the Collection Description
validator, it is not gated on the conditional-writes backend feature: a
server implementing the Collection metadata endpoints MUST maintain it and
MUST support If-Match and If-None-Match: * on the
10.9 Update Collection Metadata Operation, evaluated atomically with the
write. A Metadata object that has never been written has no validator, and
If-None-Match: * succeeds exactly then.
A 412 arises only from an explicit If-Match / If-None-Match
precondition header. It is deliberately distinct from the header-less 409
conflict kinds -- id-conflict (a POST create whose chosen id is already
taken) and reserved-id -- which describe a conflict with current state where
the client stated no precondition. Conditional requests are the versioned,
header-driven concurrency mechanism; the 409 kinds are not.
The list operations -- 8.2 List Spaces Operation, 9.5 List All Collections operation, and 10.5 List Collection operation -- MAY paginate their responses, returning one page of items at a time using the cursor-based profile defined in Appendix A. Pagination. Pagination is OPTIONAL: a server that returns every item in a single response is conformant, and a client MUST be prepared for either behavior.
A Spaces Repository is a set of API endpoints that supports the creation and
management of multiple spaces on a given server.
This /spaces/ set of API endpoints is optional. If a server does not support
this feature (for example, if it is a single-tenant server with an existing
hardcoded Space), then it can implement only the /space/{space_id}/ endpoints
and get most of the functionality of this specification.
To create a Space:
Perform an authorized Create Space operation that includes a Proof of (cryptographic material) Possession via a mechanism such as HTTP Signatures.
If no id is provided, it will be generated by the storage server.
A controller DID MUST be provided in the request body, and the request MUST
be authorized by that DID.
controller is provided, the server MUST return an HTTP 400 error
responsecontroller:
either signed directly by the controller DID (the common case), or signed
by another DID presenting a valid, unexpired delegation chain rooted in the
controller (see 5.1.5.2 Delegation). This is how the root of trust is
initially set up (see 5.1.2 Space controller and the Root of Trust for
more details).POST capability for /spaces/ by the user,
and create a Space that the user's DID controls from the start. What is
never acceptable is installing a controller that has authorized nothing
at all: that would let anyone bind arbitrary DIDs to Spaces without their
consent -- squatting meaningful ids "in their name", or burning
per-controller onboarding allowances such as "one free Space per
controller".(Optional, out of scope) A given storage provider MAY impose additional requirements to create a Space for a given controller, such as:
To create a space via HTTP API using the Spaces Repository POST API:
POST /spaces/ HTTP/1.1
Host: example.com
Accept: application/json
Content-type: application/json
Authorization: ...
{
"type": ["Space"],
"name": "Example space #1",
"controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW"
}
Example success response:
HTTP/1.1 201 Created
Content-type: application/json
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e
{
"id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
"type": ["Space"],
"name": "Example space #1",
"controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW"
}
Note that in the example above:
id was not specified in the body of the request, and so was generated by
the server and returned in the responseErrors (see F. Error Type Registry for canonical examples):
controller property is
missing from the request body.controller DID.controller DID in the request body: it is neither signed
by that DID nor accompanied by a valid, unexpired delegation chain rooted in
it. A chain rooted in a different DID, an expired delegation, and a chain
whose proof fails verification all fall under this one type. Servers are not
required to distinguish these (delegation-chain verifiers often report
failure opaquely), but SHOULD differentiate the cause in the non-normative
detail string where they can. In a delegated provisioning flow, the
cause determines who must act (the user re-delegates an expired capability;
the service corrects a request whose chain is rooted in the wrong DID).id is not URL-safe (see
4. Identifiers).id already exists.Onboarding requirements, if any, are provider-specific and out of scope for this
specification (see the optional onboarding material above). Their error type,
title, and status code are likewise provider-defined.
A POST /spaces/ that supplies an id already in use returns the
id-conflict error. To create or replace a Space at a client-chosen
id without conflict, use the idempotent
9.3 Update (or Create by Id) Space operation instead.
The existence check behind id-conflict is security-critical here, not
merely a conformance detail. Create Space is the one operation whose capability
invocation is verified against the controller supplied in the request
body -- the Space does not exist yet, so there is no stored controller to
verify against. A server that instead treats POST /spaces/ as
create-or-replace turns this operation into a takeover: any caller able to
construct a valid invocation for their own controller could overwrite an
existing Space's description (controller included) simply by POSTing its
id. Servers MUST check for an existing Space with the supplied id, and
reject with id-conflict, before writing anything.
409 from 201 -- just as they could by observing whether creation succeeds.
This disclosure is inherent to client-chosen ids on a create endpoint, and is
not a violation of the maximum-privacy principle of 6. Error Handling,
which governs errors about *existing* targets the caller is not authorized
for. Unguessable (for example, UUID) Space ids keep the signal worthless to an
attacker, and providers that gate Space creation behind onboarding requirements
also bound who can observe it.
detail strings on controller-mismatch are likewise
privacy-safe: Create Space has no existing target to protect, and everything
its verification examines (the body's controller, the capability chain,
its signatures) is supplied by the caller, so failure granularity reveals
nothing the caller does not already hold. This reasoning does *not* extend to
failure causes that depend on server-side state (for example, capability
revocation status or per-controller onboarding allowances); whether and how to
disclose those remains provider-defined.
Requires appropriate authorization (root zcap invoked by the controller of one or more spaces, or a zcap granting permission to read a one or more spaces)
Lists only the spaces the requester is authorized to see
MAY be paginated (see A. Pagination)
Example request:
GET /spaces/ HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
Example success response (requester has read access to at least one space):
HTTP/1.1 200 OK
Content-type: application/json
{
"url": "/spaces/",
"totalItems": 1,
"items": [
{
"id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e"
}
]
}
Example success response (requester does NOT have access to any spaces):
HTTP/1.1 200 OK
Content-type: application/json
{
"url": "/spaces/",
"totalItems": 0,
"items": []
}
A request that carries no authorization (or authorization for no spaces) is not
an error: like any list operation it returns the 200 OK empty-list response
shown above, revealing nothing about which spaces exist (see
6. Error Handling).
A space is a namespace for collections and a unit of general configuration, a volume of storage that contains one or more collections. Conceptually, is maps to a disk partition (for file systems), or a database (for relational databases).
Space properties:
id - A unique identifier for a space at a given server. Created by the
server if not provided. Note: the {space_id} template parameter used in URL
templates in this spec MUST match that space's id property. See
4. Identifiers for additional constraints.type - An array of strings, MUST include the type Space. The array SHOULD
be lexically sorted so that the object has a canonical, stable serialization
(useful for hashing or signing).name (optional) - An arbitrary human-readable name for the space. Does not
have to be unique.controller - A cryptographic identifier (a did)
of the entity that is authorized to perform operations on the space (or to
delegate authorization to other entities)Space properties automatically added by the server:
createdBy (optional) - The did of the party whose capability invocation
created the space. It is recorded on the first write and preserved unchanged
by later writes, so it names the creator rather than the last writer. It is
read-only: a server MUST ignore a createdBy supplied in a request body.
createdBy is distinct from controller -- under delegated provisioning
(see 5.1.2 Space controller and the Root of Trust) the party that creates a
space need not be the party that controls it. Recording it is OPTIONAL, so a
client MUST treat an absent createdBy as "not recorded" rather than as an
assertion that the space has no creator.url - A relative URL to the space's description resource.
Added by the server, used in the Space Description object as well as
the 8.2 List Spaces Operation result.linkset - A relative URL to a resource which contains
a set of links to auxiliary resources (such as to access control policy
documents). See section 13.1 Space Linkset.
Note that this is one of the B.1 Space-level reserved endpoints.idThe format of the response is determined based on content negotiation;
application/json is the REQUIRED baseline (see
11.3 Content Types and Representations).
Example request:
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
Example success response:
HTTP/1.1 200 OK
Content-type: application/json
{
"id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e",
"type": ["Space"],
"name": "Example space #1",
"controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW",
"createdBy": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW",
"linkset": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/linkset"
}
Errors (see F. Error Type Registry for canonical examples):
When creating or modifying a Space via PUT, the client specifies the id
of the Space.
namecontrollerThe controller property in the request body is the proposed value, not the
root of trust for the request. When the Space already exists, the capability
invocation MUST be verified against the Space's currently stored
controller: only the current controller (or its delegate) may update the
Space, including transferring it by writing a new controller. The body's
controller participates in verification only when the PUT creates the
Space -- there is no stored controller yet, so, as with
8.1 Create Space operation, the invocation MUST be authorized by the body's
controller: signed by it directly, or presented with a valid, unexpired
delegation chain rooted in it (violations are controller-mismatch, as for
POST). A server that verifies an update against the
body's controller reopens the takeover described under
8.1.2 Create Space Errors: any caller could seize an existing Space by
PUTting its id with themselves as the controller.
A server that records createdBy (see 9.1 Space Data Model) sets it only on
the PUT that creates the Space, to the did of the party that invoked the
capability -- which is not necessarily the body's controller. On a PUT that
updates an existing Space, the stored createdBy is preserved, so transferring
the Space by writing a new controller does not rewrite its creator.
Note that this is a full update (partial updates via http PATCH verb might
be supported later). However, some fields cannot be updated (like id) and so
may be omitted from the request payload.
Note that this operation is idempotent.
controller property is required in the PUT
request body.Example request (creating a new space via PUT), note the lack of trailing slash:
PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e HTTP/1.1
Host: example.com
Accept: application/json
Content-type: application/json
Authorization: ...
{
"id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
"type": ["Space"],
"name": "Example space #1",
"controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW"
}
Example success response:
HTTP/1.1 201 Created
Content-type: application/json
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e
Example request (updating the name property of a space). Note that
server-managed properties such as url and linkset are not client-writable
and are omitted from the request body:
PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e HTTP/1.1
Host: example.com
Content-type: application/json
Accept: application/json
Authorization: ...
{
"id": "81246131-69a4-45ab-9bff-9c946b59cf2e",
"type": ["Space"],
"name": "Newly renamed space #1",
"controller": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW"
}
Example success response:
HTTP/1.1 204 No Content
Errors (see F. Error Type Registry for canonical examples):
id that does not match the
{space_id} in the request URL.Example request (no request body):
DELETE /space/81246131-69a4-45ab-9bff-9c946b59cf2e HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
Example success response:
HTTP/1.1 204 No Content
Errors (see F. Error Type Registry for canonical examples):
204; an under-authorized request returns 404 instead, per
6. Error Handling, so that an unauthorized caller cannot probe for
existence.id is not URL-safe.GET actionname property is optional, default it to be the same
value as id when name is missing. (The name is intended to drive UIs, so
defaulting to id simplifies consuming client logic.)id, url, and name. A server
MAY additionally surface a public member on each item: a boolean indicating
whether a PublicCanRead access control policy (see
5.1.7 Access Control Policies) is attached to that Collection. It is surfaced
inline so that a listing consumer need not issue one policy probe per item to
learn whether the Collection is publicly readable. A server that surfaces
public MUST include it on every item in the listing, expressing false
explicitly rather than omitting it, so that a client can distinguish "not
public" from "the server does not surface the flag". Consistent with the
fail-closed rule of the policy evaluation contract, a policy whose type the
server does not recognize MUST be reported as "public": false. When public is absent
from a listing, a client falls back to probing each Collection's policy (see
5.1.7 Access Control Policies).Example request (note the trailing slash):
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/collections/ HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
Example response:
HTTP/1.1 200 OK
Content-type: application/json
{
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/collections/",
"totalItems": 2,
"items": [
{
"id": "example",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/example",
"name": "Example Collection",
"public": false
},
{
"id": "73WakrfVbNJBaAmhQtEeDv",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv",
"name": "73WakrfVbNJBaAmhQtEeDv",
"public": true
}
]
}
Errors (see F. Error Type Registry for canonical examples):
A collection is a namespace for Resources, and a unit of configuration, within a space.
In other storage systems, the concept of collections has many different names. For example, Directory, Folder, RDBMS Table, Document Collection, Graph, WebAPI FileList, Bucket, LDP Basic Container, EDV Vault, and so on.
Collections do not contain other collections (this specification adopts a flat collection structure).
query endpoint with field-based filtering.
Collection properties (user-writable):
id - A unique collection identifier (within a given space). Created by the
server if not provided. Note: the {collection_id} template parameter used in
URL templates in this spec MUST match that collection's id property.
See 4. Identifiers for additional constraints.type - An array of strings, MUST include the type Collection. As with a
Space's type, the array SHOULD be lexically sorted for a canonical, stable
serialization.name (optional) - An arbitrary human-readable name for the collection. Does not
have to be unique.generator (optional) - The did of the application this collection was
provisioned for, named for the ActivityStreams 2.0 generator term (the
application that generated an object). It is the client-asserted identity
axis of 11.8.1 Writer attribution: writerId and createdBy: client-supplied, persisted, and writable
by the Space controller on create and on update -- updatable so that a
controller can backfill the property onto collections that predate it --
in contrast to the server-observed, read-only createdBy. The contrast is
the reason the property exists: under delegated provisioning the party
whose capability invocation creates the collection is the user's agent (a
wallet), so createdBy records the user's did, never the
application's, and only the controller is in a position to name the
application it provisioned the collection for. generator is an assertion
by the controller, not a server-verified fact: a server MUST NOT verify,
compute, or default it, and MUST NOT use it as an input to authorization.
A server MAY separately record that the generator did has been
observed invoking a delegated capability on the collection -- corroboration
on the same evidentiary footing as createdBy (server-observed use, not
verified identity). Either way, any reader other than the controller (a
delegated consumer of a shared collection, a reader of a world-readable
one) MUST treat generator as exactly a controller assertion. A present
but empty or non-did-shaped value is an invalid-request-body
error.generatorOrigin (optional) - The Web origin (its ASCII serialization,
e.g. https://app.example.com) the generator did was bound to when
the collection was provisioned. A provisioning exchange in which the
user's agent authenticates the requesting application by origin (for
example a browser-attested credential-handler exchange) establishes an
origin binding the storage server is never a party to; stamping the origin
beside generator preserves that fact and gives a reader a human-readable
attribution label without further lookups. Same footing as generator in
every other respect: controller-asserted, writable by the Space controller
on create and update, never verified or defaulted by the server. A present
value that is not the ASCII serialization of a Web origin (a URL carrying
a path, query, or fragment; the empty string) is an
invalid-request-body error. generatorOrigin SHOULD NOT be present
without generator.backend (optional) - An object describing the storage backend selected for
this collection. If not specified, defaults to the value { "id": "default" }.
The backend object's id property MUST be from the list of 14.2 Space Backends Available
for the given space. If an unavailable (unsupported) backend is specified,
the server MUST throw an error.
See section 14. Backends for more details.encryption (optional) - A non-secret descriptor declaring that this collection's
Resources are client-side encrypted, and naming the scheme. The value is an
object with a required string scheme and an optional positive-integer
version (e.g. { "scheme": "edv", "version": 1 } for the EDV-over-WAS
scheme); an absent version means 1, and an absent descriptor means the
collection is a plaintext collection -- its
Resources are stored as the client sent them, with no client-side encryption
envelope. The version names the version of the
scheme's wire format as registered in the C. Encryption Scheme Registry,
so a scheme's envelope profile can evolve without ambiguity about which shape
a given collection stores; it is an opaque registry key with a total order,
not a semantic version. The server
MUST NOT interpret the descriptor beyond validating its shape -- it never holds key
material and stores the descriptor opaquely. Its purpose is discovery: any
authorized reader (including a delegated consumer that did not create the
collection) learns from the Collection Description that the collection is
encrypted, and decrypts with its own keys. The descriptor is set-once,
version-monotonic: a server
MUST allow declaring it on a collection that lacks one (e.g. migrating a
pre-existing collection), and MUST accept a re-declaration of the standing
values (including an explicit "version": 1 on a descriptor that had omitted
it) as an idempotent no-op, but MUST reject (with an encryption-immutable error)
any attempt to change the scheme, decrease or remove the version, or
clear the descriptor, on an existing
collection, since that would corrupt the already-stored encrypted Resources.
On a collection whose stored descriptor
carries an explicit "version": 1, an update whose descriptor omits
version counts as an attempt to remove it and is rejected under this rule,
though because an absent version means 1, a server MAY instead accept
such an update as an idempotent re-declaration of the standing value. Under
either reading, the standing version survives unchanged.
Raising the version is permitted (subject to the recognition rule in
C. Encryption Scheme Registry): subsequent writes are validated against
the new profile, while already-stored Resources are unaffected.
See section 14. Backends (client-side encryption note) for the rationale.
A server that recognizes the declared scheme and version enforces them
structurally on write -- rejecting any non-envelope body so plaintext can never
be stored in an encrypted Collection; see C. Encryption Scheme Registry.
On the edv scheme, the descriptor MAY additionally carry the key-epoch
public references, currentEpoch and epochs (see C.4 Key Epochs),
which let several readers hold per-reader keys and let reader removal rotate
the collection key. These fields contain only public keys and wrapped-key
ciphertext; as with the rest of the descriptor, the server stores them
opaquely and validates only their shape and a small set of integrity rails.
Client-side profiles built on epochs (such as [WAS-EC]) require these
members from the descriptor's creation onward; the server-side optionality
here exists because key management is out of this specification's scope, and
is not a license for such a profile's clients to operate an epoch-less
descriptor.Collection properties automatically added by the server:
createdBy (optional) - The did of the party whose capability invocation
created the collection, on the same terms as a Space's createdBy (see
9.1 Space Data Model): recorded on the first write, preserved by later
writes, read-only, and OPTIONAL.url - A relative URL to the collection's description resource.
Added by the server, used in the Collection Description object as well as
the List Collections operations result.linkset - A relative URL to a resource which contains
a set of links to auxiliary resources (such as to access control policy
documents). See section 13.2 Collection Linkset.
Note that this is one of the B.2 Collection-level reserved endpoints.Each Collection also has an associated Metadata object, distinct from this
Collection Description -- server-managed properties (optional timestamps and
createdBy) plus user-writable ones (name and tags, nested under
custom) -- addressable at the reserved meta path segment under the
Collection URL. See 10.7 Collection Metadata Data Model.
Example collection (JSON representation):
{
"id": "73WakrfVbNJBaAmhQtEeDv",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv",
"type": ["Collection"],
"name": "Verifiable Credentials Collection",
"createdBy": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW",
"generator": "did:key:z6MkfriqYRX3JBqzsbVbKuBUxDR2nsjLTu6AbrxJZAmFmXWb",
"generatorOrigin": "https://app.example.com",
"linkset": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/linkset"
}
When a Collection is created via a POST, the client can specify the id of
the Collection. If the id is not specified, one is auto-generated by the
server and returned as part of the Location response header.
Example request (id not specified, auto-generated by the server and returned
in the response Location header):
POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/collections/ HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...
{
"name": "Verifiable Credentials Collection",
"type": ["Collection"]
}
In this example, the id in the body is not specified, and will be auto-generated
by the server. The backend is not specified, and will be assigned the default
value.
Example response:
HTTP/1.1 201 Created
Content-type: application/json
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/21f81693-f4a3-4caa-b81c-b663d6e1e3ae
Example request, a valid id specified in the body:
POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/collections/ HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...
{
"id": "credentials",
"name": "Verifiable Credentials Collection",
"type": ["Collection"],
"backend": { "id": "default" }
}
Example response:
HTTP/1.1 201 Created
Content-type: application/json
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/credentials
Errors (see F. Error Type Registry for canonical examples):
id collides with one of the
B.1 Space-level reserved endpoints (for example, query or collections).id already exists.
To create or replace a Collection at a client-chosen id without conflict,
use the idempotent 10.3 Update (or Create By Id) Collection operation instead.
Servers MUST perform this existence check only after the caller's
authorization has been verified: an under-authorized caller receives the
privacy-merged not-found (404) per 6. Error Handling, so that it
cannot use the 409 to probe a Space for existing Collection ids.backend id is not in that
space's 14.2 Space Backends Available list.When creating or modifying a Collection via PUT, the client specifies the id
of the Collection. This Collection id MUST NOT collide with the list of
B.1 Space-level reserved endpoints.
Example successful "create" request (note the lack of trailing slash):
PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...
{
"id": "73WakrfVbNJBaAmhQtEeDv",
"name": "Verifiable Credentials Collection",
"type": ["Collection"],
"backend": { "id": "default" }
}
HTTP/1.1 201 Created
Errors (see F. Error Type Registry for canonical examples):
id collides with one of the
B.1 Space-level reserved endpoints (for example, collections or
linkset).scheme,
decrease or remove the version, or clear an
existing encryption descriptor (the descriptor is set-once,
version-monotonic; see
10.1 Collection Data Model).epochs append-only,
currentEpoch never moving backwards); see C.4 Key Epochs.If-Match
precondition and the description's current ETag does not match it (see
7.2 Conditional Requests).GET actionkey-epochs feature, the response includes an
ETag header over the description's version, for use with If-Match on a
subsequent update (see 7.2 Conditional Requests)Example request (note: no trailing slash):
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
Example response:
HTTP/1.1 200 OK
Content-type: application/json
{
"id": "73WakrfVbNJBaAmhQtEeDv",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv",
"name": "Verifiable Credentials Collection",
"type": ["Collection"],
"linkset": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/linkset",
"backend": { "id": "default" }
}
Errors (see F. Error Type Registry for canonical examples):
GET actionnext link
and cursor continuation.Example request (note the trailing slash):
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/ HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
Example response:
HTTP/1.1 200 OK
Content-type: application/json
{
"id": "73WakrfVbNJBaAmhQtEeDv",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv",
"name": "Example JSON Documents Collection",
"type": ["Collection"],
"totalItems": 2,
"items": [
{
"id": "321efd4e-23cb-497c-aaee-7bd26e66d39e",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/321efd4e-23cb-497c-aaee-7bd26e66d39e",
"contentType": "application/json"
},
{
"id": "3943c87f-b617-44bc-ba75-8de2b16c3640",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/3943c87f-b617-44bc-ba75-8de2b16c3640",
"contentType": "application/json"
}
]
}
A client requests a bounded page with limit (here, two items from a Collection
that holds more):
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/?limit=2 HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
The response carries the page of items plus a next link, signalling that more
items follow. totalItems is omitted here -- the server does not return a total
for this (potentially large) Collection:
HTTP/1.1 200 OK
Content-type: application/json
{
"id": "73WakrfVbNJBaAmhQtEeDv",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv",
"name": "Example JSON Documents Collection",
"type": ["Collection"],
"items": [
{
"id": "321efd4e-23cb-497c-aaee-7bd26e66d39e",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/321efd4e-23cb-497c-aaee-7bd26e66d39e",
"contentType": "application/json"
},
{
"id": "3943c87f-b617-44bc-ba75-8de2b16c3640",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/3943c87f-b617-44bc-ba75-8de2b16c3640",
"contentType": "application/json"
}
],
"next": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/?limit=2&cursor=eyJhZnRlciI6IjM5NDNjODdmLWI2MTctNDRiYy1iYTc1LThkZTJiMTZjMzY0MCJ9"
}
The client follows next verbatim to fetch the subsequent page (the cursor is
opaque and supplied by the server -- the client does not build it):
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/?limit=2&cursor=eyJhZnRlciI6IjM5NDNjODdmLWI2MTctNDRiYy1iYTc1LThkZTJiMTZjMzY0MCJ9 HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
The final page omits next, marking the end of the list:
HTTP/1.1 200 OK
Content-type: application/json
{
"id": "73WakrfVbNJBaAmhQtEeDv",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv",
"name": "Example JSON Documents Collection",
"type": ["Collection"],
"items": [
{
"id": "9c7b2e51-0d4a-4c2f-8b3e-1f6a5d8e7c90",
"url": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/9c7b2e51-0d4a-4c2f-8b3e-1f6a5d8e7c90",
"contentType": "application/json"
}
]
}
Where a Resource carries a key-epoch stamp (see C.4 Key Epochs), its item
summary additionally carries it as epoch (optional), mirroring the Resource
Metadata property -- so a reader walking a listing can select its epoch key
without a per-Resource metadata fetch. A writer-attribution label (see
11.8.1 Writer attribution: writerId and createdBy) is likewise carried as writerId (optional),
mirroring the Resource Metadata property.
On a Collection that declares an encryption descriptor (see
C. Encryption Scheme Registry), each item's user-writable metadata is stored
encrypted (see 11.8 Resource Metadata Data Model). Item summaries for an
encrypted Collection therefore carry only server-visible fields (id, url,
contentType, and, where declared, epoch and writerId) and omit name; a
client that needs names decrypts each Resource's Metadata itself.
Errors (see F. Error Type Registry for canonical examples):
Requires appropriate authorization
DELETE action.This operation is idempotent
(Assuming the request carries appropriate authorization) Sending a DELETE request to a collection that does not exist (or has already been deleted) results in a 204 success response
Example request (note the lack of trailing slash):
DELETE /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv HTTP/1.1
Host: example.com
Authorization: ...
Example success response:
HTTP/1.1 204 No Content
Each Collection has an associated Metadata object of its own, addressable
at the reserved meta path segment under the Collection URL (one of the
B.2 Collection-level reserved endpoints). Because that segment occupies the
{resource_id} position, meta is a reserved Resource id: creating a
Resource with the id meta is a reserved-id conflict (see
B. Reserved Path Segment Registry). A GET or PUT at this path is
therefore always the Collection metadata operation, never a Resource
operation; the reserved-id rejection arises where a Resource id is
supplied explicitly, as in a POST body's id. Methods this specification
does not define at this path (DELETE in particular) are not Resource
operations either: a server MAY answer them with 405 Method Not Allowed,
or -- treating the request as a Resource operation on a reserved id -- with a
reserved-id (409) error, but MUST NOT let them act on a stored Resource.
The Collection metadata endpoints are
OPTIONAL on the same terms as the Resource-level ones; a server that does
not implement them SHOULD return an unsupported-operation (501) error for
requests to these paths.
The object mirrors the 11.8 Resource Metadata Data Model: server-managed
properties at the top level, user-writable properties nested under custom,
and epoch as a writable sibling of custom. The differences follow from
what a Collection is -- a container, not a document. There are no
contentType and size properties, because those describe a stored
representation and a Collection has none.
Server-managed properties (all OPTIONAL):
createdAt (optional) - The [RFC3339] date-time at which the Metadata
object was first written. It describes the Metadata object, not the
Collection: a Collection may exist long before its metadata is first
written, so createdAt here can postdate the Collection's creation by
arbitrarily long.updatedAt (optional) - The [RFC3339] date-time at which the Metadata
object was last modified.createdBy (optional) - The did of the party whose capability
invocation created the Collection, on the same terms as the
createdBy of the 10.1 Collection Data Model (it is the same value,
surfaced here so a metadata read is self-contained). Unlike the
timestamps, it describes the Collection rather than the Metadata object,
and it is present (when recorded at all) from the Collection's creation
onward.User-writable properties:
custom (optional) - A JSON object holding the user-writable properties,
with the same shape as the Resource-level custom: an optional
human-readable name and an optional tags object of string-valued
annotations. A Metadata object with no user-writable properties set MAY
omit custom or report it as {}. A Collection therefore has two name
slots: the name of the 10.1 Collection Data Model is server-visible
plaintext and is what listings report, while custom.name here is
supplementary -- except on an encrypted Collection, where a client SHOULD
leave the Description's plaintext name unpopulated (a listing that
defaults a missing name to the id then reveals nothing; see
9.5 List All Collections operation) and custom.name, inside the
encrypted envelope, is the only name the Collection has.epoch (optional) - The key-epoch id this Metadata object's custom
envelope was encrypted under (see C.4 Key Epochs), declared by the
writer as a top-level member of an Update Collection Metadata request and
stored opaquely; the server never computes or verifies it. Its omission
rule deliberately inverts the Resource-level one: an omitted epoch on
a Collection metadata write clears the stored value (at Resource level
it is preserved). The Resource-level epoch property describes the
content write, which a metadata write does not touch, so preserving it is
correct; here the only thing epoch can describe is the custom
envelope itself, which every metadata write replaces wholesale, so a
preserved value would misdescribe the new envelope. A client writing to
an encrypted Collection therefore re-declares epoch on every metadata
write.There is no writerId member: Collection metadata is configuration
maintained by the Space controller, not replicated per-revision content,
so the attribution machinery of 11.8.1 Writer attribution: writerId and createdBy does not apply at
this level.
On a Collection that declares an encryption descriptor (see
C. Encryption Scheme Registry), custom is stored encrypted, exactly
as at Resource level: its value is an envelope of the Collection's declared
scheme, validated structurally on write (rejecting a plaintext custom with
an encryption-scheme-mismatch) and never decrypted by the server, while
the server-managed properties and epoch remain plaintext. This is what
gives an encrypted Collection a client-encrypted display name and tags, and a
discoverable, conditionally-writable home for profile-level configuration
such as the [WAS-EC] blinded-index schema. One consequence: on an
encrypted Collection every metadata write MUST carry a conforming envelope as
its custom, so the plaintext clearing convention (a PUT with no custom;
see 10.7.2 Lifecycle) is unavailable -- the cleared state
is instead an envelope encrypting an empty object.
The Metadata object carries its own monotonic version,
metaVersion, independent of both the Collection Description's validator and
every Resource's: writing one never bumps another. (The name is the same one
the changes query profile reports per Resource;
this is its Collection-scoped counterpart.) A server implementing
these endpoints MUST maintain it and expose it as a strong ETag validator
on read, and MUST honor If-Match / If-None-Match: * preconditions on
update, evaluated atomically with the write; see 7.2 Conditional Requests.
The version is surfaced only as the ETag: it is not a member of the
Metadata object's JSON representation.
A Metadata object that has never been written carries no validator (a read
returns no ETag), and If-None-Match: * succeeds exactly then -- it is the
"write only if never written" precondition. This gives a
read-reconcile-write client (for example, one maintaining the [WAS-EC]
blinded-index schema in this envelope) lost-update protection without
involving the Collection Description or any Resource. Collection metadata
writes are, however, invisible to replication: the
changes query profile is per-Resource and emits
no entry for them, so a client mirroring this object re-reads it and
compares the returned ETag.
The Metadata object is created and deleted together with the
Collection: a GET of /meta on an existing Collection always succeeds,
even before the first metadata write (reporting only createdBy, when
recorded -- no timestamps, no custom, no ETag). There is no
DELETE /meta operation; to clear the user-writable properties, send a PUT
with an empty custom object or, on a plaintext collection, an empty
body object (on an encrypted Collection the cleared state is an envelope
encrypting an empty object, as above). Clearing is itself a
write and advances metaVersion, so a cleared Metadata object remains
distinguishable from a never-written one. Deleting the Collection removes
its Metadata object with it, and a Collection later re-created under the same
id starts over with never-written metadata -- its metaVersion restarts, so
a client MUST NOT compare validators across a delete and re-create.
GET actionETag header over its metaVersion, for use with If-Match
on a subsequent update (see 7.2 Conditional Requests)Example request:
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/meta HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
Example success response:
HTTP/1.1 200 OK
Content-type: application/json
ETag: "2"
{
"createdAt": "2026-06-10T09:12:00Z",
"updatedAt": "2026-06-12T13:25:00Z",
"createdBy": "did:key:z6MkpBMbMaRSv5nsgifRAwEKvHHoiKDMhiAHShTFNmkJNdVW",
"custom": {
"name": "Verifiable Credentials Collection",
"tags": { "project": "demo" }
}
}
Errors (see F. Error Type Registry for canonical examples):
As at Resource level, a PUT to the Collection's /meta endpoint is a
full replacement of the Metadata object's custom object: the stored
custom object is replaced by the one in the request body, so any
user-writable property omitted from it is cleared (and, on a
plaintext collection, a request body with no custom property clears
them all).
The only other writable top-level member is epoch, whose omission
clears the stored stamp (see the 10.7 Collection Metadata Data Model
for the rule and its rationale -- this is the inverse of the Resource-level
behavior). There is no writerId member at this level.
Server-managed properties are not affected by this operation; a server MUST
ignore any top-level properties other than custom and epoch present in
the request body (so that a client may read the Metadata object, modify it,
and PUT it back without first stripping the server-managed properties).
A PUT to a Collection's /meta does not create anything: a Metadata
object cannot exist apart from its Collection, so a PUT to the /meta
path of a nonexistent Collection returns a not-found (404) error.
PUT actionIf-Match or If-None-Match: * precondition against the
Metadata object's own validator (see 7.2 Conditional Requests)metaVersion validator)204 success response, with an ETag header carrying the new
validatorExample request:
PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/73WakrfVbNJBaAmhQtEeDv/meta HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...
{
"custom": {
"name": "Verifiable Credentials Collection",
"tags": { "project": "demo" }
}
}
Example success response:
HTTP/1.1 204 No Content
ETag: "3"
Errors (see F. Error Type Registry for canonical examples):
custom object (or a property within it) does not have the shape
described in 10.7 Collection Metadata Data Model, or a top-level epoch
member is present but is not a non-empty string.custom is not a conforming envelope of the Collection's
declared scheme (including the case of an omitted custom, since an
encrypted Collection's metadata cannot be cleared to a plaintext state;
see 10.7 Collection Metadata Data Model).If-Match / If-None-Match: *
precondition evaluated false; see 7.2 Conditional Requests.A unit of data, in transit or at rest. (As described in the W3C FileAPI: Blob Interface).
Blob properties:
size - length of the byte stream in bytestype (from the IANA mime type registry) - If not specified, defaults to
application/octet-streamWhen a Blob is stored as a Resource, its type and size are reported as
the contentType and size properties of the Resource's Metadata object
(see 11.8 Resource Metadata Data Model), and the stored bytes are returned
verbatim on reads (see 11.3 Content Types and Representations).
A resource is a named (addressable) Blob stored in a given collection, with metadata. The data model is derived from W3C FileAPI: File Interface, but with the addition of a few crucial properties.
In similar storage systems, a resource is called "File", "Object", "Document", "Row", "Graph", and so on.
Resource properties:
id - A unique identifier for a resource for a given collection. Created by the
server if not provided. Note: the {resource_id} template parameter used in URL
templates in this spec MUST match that resource's id property (as returned by
the 10.5 List Collection operation).name - (optional) - Human-readable name for the resource, useful for building
user interfaces for browsing collection contents.type (optional) - An array of strings describing the resource. Unlike Space
and Collection, no specific type value is required of a Resource.url (optional) - A relative URL (provided by the server when listing resources
via the 10.5 List Collection operation)contentType (optional) - The MIME type of the default representation of the
resource (provided by the server when listing resources
via the 10.5 List Collection operation)contentType, size, optional timestamps) plus user-writable
ones (name and tags, nested under custom) -- addressable at the
reserved /meta path segment under the Resource URL. See
11.8 Resource Metadata Data Model.A Resource has exactly one current representation: the stored bytes plus the content type they are stored under. Every successful write (POST or PUT) replaces that representation entirely -- including replacing a representation previously stored under a different content type. Servers are not required to store, convert between, or negotiate multiple representations of a Resource.
Writing. A Resource write request MUST carry a Content-Type header; a
server MUST reject a write without one with a missing-content-type (400)
error. The request body is interpreted according to its content type:
application/json, or any type with a
+json structured-syntax suffix [RFC6839] (for example,
application/ld+json), is stored as a JSON document.application/octet-stream.multipart/form-data [RFC7578] uploads (the HTML form workflow). The
request MUST contain exactly one file part; that part supplies the stored
bytes, and its own content type (not multipart/form-data) becomes the
stored content type. Because a write targets a single Resource, there is no
batch form: a server MUST reject a multipart request with no file part, or
with more than one, with an invalid-request-body (400) error.Reading. A GET returns the current representation, verbatim, with the
stored content type as the response Content-Type. Because Resources are
single-representation, the request's Accept header is advisory: a server
MAY ignore it, and MUST NOT reject a Resource read for lack of an acceptable
representation (no 406 Not Acceptable) -- the stored representation is
always the answer. A HEAD request returns the same headers without the
body; the response Content-Type and Content-Length correspond to the
contentType and size properties of the Resource's Metadata object (see
11.8 Resource Metadata Data Model).
Description objects. API documents generated by the server -- Space and
Collection descriptions, listings, quota reports, Metadata objects -- MUST be
available as application/json. Linkset documents use
application/linkset+json [RFC9264], and error responses use
application/problem+json [RFC9457] (see 6. Error Handling). Other
representations of these documents (negotiated via Accept) MAY be offered
in addition to, never instead of, the JSON baseline.
The request MAY include a Key-Epoch header declaring the key epoch the
body was encrypted under (see C.4 Key Epochs); the value is stored as the
Resource Metadata epoch property. When absent, any stored epoch stamp is
cleared. The request MAY likewise include a Writer-Id header declaring the
writing agent's attribution label (see 11.8.1 Writer attribution: writerId and createdBy); the value
is stored as the Resource Metadata writerId property, and when absent any
stored writerId is cleared.
Example request (adds a JSON object to the messages collection).
Note that since no Resource id was specified, the server auto-generated an id
and returned it as part of the Location response header.
POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/ HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...
{"message":"hi"}
Example success response:
HTTP/1.1 201 Created
Content-type: application/json
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/6b5be748-5f39-4936-a895-409e393c399c
Example request (multipart upload of a photo to the photos collection, on
a server that supports the OPTIONAL multipart form; see
11.3 Content Types and Representations). The stored content type is the
file part's own type, image/png, not multipart/form-data:
POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/photos/ HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----boundary314
Authorization: ...
------boundary314
Content-Disposition: form-data; name="file"; filename="sunset.png"
Content-Type: image/png
...binary PNG bytes...
------boundary314--
Example success response:
HTTP/1.1 201 Created
Location: https://example.com/space/81246131-69a4-45ab-9bff-9c946b59cf2e/photos/d2887cf0-186f-4e2e-a575-c9ce1b9d8a45
Errors (see F. Error Type Registry for canonical examples):
Content-Type header (see 11.3 Content Types and Representations).id collides with one of the
B.2 Collection-level reserved endpoints (for example, query or
linkset).id already exists.
To create or replace a Resource at a client-chosen id without conflict,
use the idempotent 11.6 Update (or Create By Id) Resource Operation instead.
Servers MUST perform this existence check only after the caller's
authorization has been verified: an under-authorized caller receives the
privacy-merged not-found (404) per 6. Error Handling, so that it
cannot use the 409 to probe a Collection for existing Resource ids.maxUploadBytes constraint (see 15. Quotas).A read returns the Resource's single stored representation, with the stored
content type; see 11.3 Content Types and Representations for content type
and Accept header handling (including the HEAD variant).
GET actionExample request to retrieve a resource:
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Accept: application/json
Example success response:
HTTP/1.1 200 OK
Content-type: application/json
{"message":"hi"}
Errors (see F. Error Type Registry for canonical examples):
When creating or modifying a Resource via PUT, the client specifies the id
of the Resource. This Resource id MUST NOT collide with the list of
B.2 Collection-level reserved endpoints.
PUT action204 success responseThe request MAY include a Key-Epoch header declaring the key epoch the
body was encrypted under (see C.4 Key Epochs); the value is stored as the
Resource Metadata epoch property. When absent, any stored epoch stamp is
cleared. The request MAY likewise include a Writer-Id header declaring the
writing agent's attribution label (see 11.8.1 Writer attribution: writerId and createdBy); the value
is stored as the Resource Metadata writerId property, and when absent any
stored writerId is cleared.
Example request to create a resource via PUT:
PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...
{"message":"hi"}
Example success response:
HTTP/1.1 204 No Content
Example request to update the created resource via PUT:
PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...
{"message":"no I've changed my mind"}
Example success response:
HTTP/1.1 204 No Content
Example request (uploading a binary Blob via PUT; the bytes are stored
verbatim under the supplied content type, see
11.3 Content Types and Representations). The Digest header binds the
body to the request signature when using the WAS Authorization Profile (see
5.1.4 Request Body Integrity (Digest Header)):
PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/photos/sunset.png HTTP/1.1
Host: example.com
Content-Type: image/png
Digest: mh=uEiCPO-qYr-z0GYV5F75-N1l8Rhjv4xIkKZsnbTZeZ7emSA
Authorization: ...
...binary PNG bytes...
Example success response:
HTTP/1.1 204 No Content
Errors (see F. Error Type Registry for canonical examples):
Content-Type header (see 11.3 Content Types and Representations).id collides with one of the
B.2 Collection-level reserved endpoints (for example, query or
linkset).maxUploadBytes constraint (see 15. Quotas).If-Match /
If-None-Match precondition evaluated false, on a backend that advertises
the conditional-writes feature (see 7.2 Conditional Requests).This operation accepts the If-Match / If-None-Match write preconditions
described in 7.2 Conditional Requests when the target backend advertises the
conditional-writes feature.
Requires appropriate authorization
DELETE actionThis operation is idempotent
(Assuming the request carries appropriate authorization) Sending a DELETE request to a resource that does not exist (or has already been deleted) results in a 204 success response
The request MAY include a Writer-Id header declaring the deleting agent's
attribution label (see 11.8.1 Writer attribution: writerId and createdBy): a deletion is a revision
like any other, and where the server keeps a tombstone the label it carries
attributes the deletion (see the tombstone notes of the
changes profile), on the same declare-or-clear
terms as a content write.
Example request to delete a resource via DELETE:
DELETE /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world HTTP/1.1
Host: example.com
Authorization: ...
Example success response:
HTTP/1.1 204 No Content
Errors (see F. Error Type Registry for canonical examples):
204; a request that lacks sufficient authorization instead
returns 404, per 6. Error Handling, so that an unauthorized caller
cannot probe for existence.Each Resource has an associated Metadata object, addressable at the
reserved /meta path segment under the Resource URL (one of the
B.3 Resource-level reserved endpoints). The metadata endpoints are
OPTIONAL; a server that does not implement them SHOULD return an
unsupported-operation (501) error for requests to /meta paths.
(A Collection has a Metadata object of its own, at the reserved meta
segment under the Collection URL; see 10.7 Collection Metadata Data Model.)
The Metadata object is a JSON document containing two groups of properties:
custom property and
are set by the client via the 11.10 Update Resource Metadata Operation.
Keeping them in their own object makes the writable surface explicit and
leaves the top level free for future server-managed properties.Server-managed properties (contentType and size are REQUIRED in every
Metadata object; the timestamps and createdBy are OPTIONAL):
contentType - The MIME type of the stored representation of the Resource,
recorded from the Content-Type header of the request that last wrote the
Resource's content (application/octet-stream if none was provided). The
same value is reported in 10.5 List Collection operation results and as
the Content-Type header of GET/HEAD responses for the Resource itself.size - The length in bytes of the stored representation of the Resource
(see the size property of the 11.1 Blob Data Model).createdAt (optional) - The [RFC3339] date-time at which the Resource
was created.updatedAt (optional) - The [RFC3339] date-time at which the Resource's
content or user-writable metadata was last modified.createdBy (optional) - The did of the party whose capability invocation
created the Resource. It is recorded on the first write and preserved
unchanged by later writes, so it names the creator rather than the last
writer -- in a Collection written by several delegated agents, createdBy
and the party responsible for the current content need not be the same. Like
the other server-managed properties it is read-only: it cannot be set through
the 11.10 Update Resource Metadata Operation. Recording it is OPTIONAL, so a
client MUST treat an absent createdBy as "not recorded" rather than as an
assertion that the Resource has no creator.etag / version property. When a backend advertises the
conditional-writes feature it exposes a Resource version as an HTTP ETag
validator and honors If-Match / If-None-Match preconditions; see
7.2 Conditional Requests. A backend without that feature performs
unconditional last-writer-wins upserts. The broader Transaction (multi-write
atomic) mechanism remains deferred.
User-writable properties:
custom (optional) - A JSON object holding the user-writable properties
below. A Metadata object with no user-writable properties set MAY omit
custom or report it as {}.name (optional) - A human-readable name for the Resource. This is the
same name property defined in the 11.2 Resource Data Model.tags (optional) - A JSON object of application-defined annotations.
Keys are strings chosen by the application; values SHOULD be strings, so
that tags stay cheap to index and filter on. Applications that need
richer structured metadata SHOULD store it as a Resource in its own
right rather than in tags.epoch (optional) - The key-epoch id this Resource's content was
encrypted under (see C.4 Key Epochs). Declared by the writer via the
Key-Epoch header on a content write or a top-level epoch member on
an Update Resource Metadata request, and stored opaquely. The server
never computes or verifies it. epoch is a sibling field alongside custom,
since on an encrypted Collection custom is the opaque envelope and is replaced
wholesale by every metadata write. An omitted epoch on a metadata write
preserves the stored value; a content write without the header clears it.writerId (optional) - An opaque label naming the writing agent that
produced the Resource's current revision (see 11.8.1 Writer attribution: writerId and createdBy
for its semantics, its deliberate separation from createdBy, and its
privacy considerations). The writer declares it, via the Writer-Id
header on a content write or a delete, or a top-level writerId member
on an Update Resource Metadata request, and the server stores it
verbatim. The value is not verified, and it MUST NOT be an input to any
authorization decision. Like epoch, it is a sibling of custom rather
than part of it. Unlike epoch, whose stamp describes the content
write, writerId describes the latest revision whichever operation
produced it: every write either declares the writer's own label or
clears the stored one. A write that declares no writerId clears the
stored value, since attribution to a bygone writer is worse than no
attribution at all.On a Collection that declares an encryption descriptor (see
C. Encryption Scheme Registry), the user-writable custom object is stored
encrypted: its value is an envelope of the Collection's declared scheme
(the same envelope profile used for a Resource's content). The server-managed
top-level properties (contentType, size, the timestamps, and createdBy)
remain plaintext -- the server needs them for listings, GET/HEAD headers,
quotas, and, in the case of createdBy, because it is the very identity the
server authenticated on the creating write. The server
validates the custom envelope structurally on write (rejecting a plaintext
custom with an encryption-scheme-mismatch) and never decrypts it; a client
holding the keys decrypts custom back to { name, tags, ... } after reading.
Because the server cannot read an encrypted name, 10.5 List Collection operation
summaries for an encrypted Collection carry no name.
A Resource's Metadata object is created and deleted together with the
Resource itself: it comes into existence (with only server-managed
properties) when the Resource is created and is removed when the Resource is
deleted. There is no DELETE /meta operation; to clear the user-writable
properties, send a PUT with an empty custom object ({ "custom": {} })
or an empty body object ({}).
createdBy and writerId (both optional) answer different questions, and
neither can substitute for the other:
createdBy is a keyed, server-verified identity: the did the
server itself authenticated on the creating capability invocation. It is
server-managed, read-only, and names the creator only; later writes
never change it.writerId is an unkeyed, client-declared attribution label: an opaque
string the writing agent volunteers about itself, naming which writing
agent produced the current revision. The server stores and serves it
verbatim and MUST NOT verify it, MUST NOT compute or default it, and MUST NOT use it as an input to authorization or any other server decision. It
is advisory replication metadata, nothing more.These two leave room for a third, distinct axis: a client-asserted identity
-- a property whose value is a did the controller writes and maintains
about another party. The Collection-level generator property (see
10.1 Collection Data Model), naming the application a Collection was
provisioned for, is this axis. Unlike writerId it is an
identity claim and a stable join key; unlike createdBy it is a controller
assertion rather than a server-verified fact, and a reader treats it as
exactly that.
In every usage, including the Writer-Id request header and the top-level
writerId member of an Update Resource Metadata request, the label is an opaque
non-empty string. A present but empty or malformed value is an
invalid-request-body error.
The reason writerId is necessary: several distinct writing agents may share
one invoking did (for example, the same delegated capability held by a
user's several installs of one app), so createdBy (or any other
server-verified identity) structurally cannot distinguish exactly the
writers that most often race each other. A per-revision label lets
replication clients break same-timestamp last-writer-wins ties with a
shared, deterministic (updatedAt, writerId) sort key. It also helps them
recognize their own writes echoed back on the
changesfeed without a fetch (on an encrypted
Collection, without a decrypt), and attribute revision history for display.
Requirements on the writing client:
writerId MUST NOT be derived from any secret, key material, or other
identity: it is an attribution label, never an identity, and it MUST NOT
be treated as one (that is the createdBy axis). A random value minted
per writing agent is RECOMMENDED.writerId it read with its own label (or omit the member)
rather than echoing the previous writer's back.Privacy considerations: unlike the encrypted custom object, writerId is
plaintext to the server, and a stable per-agent label reveals to the server
(and to any reader of the Collection) how many writing agents stand behind
one invoking did, durably per revision -- information the server could
otherwise only approximate from traffic analysis. This is accepted as the
explicit price of pre-fetch echo suppression, which only a server-visible
field can provide, and it is bounded: the label reveals the multiplicity of
one principal's own writing agents, not any cross-party relationship (the
axis the C.4 Key Epochs recipients/grantees separation protects). And
because it is random, unkeyed, and clearable, it cannot be linked across
users or upgraded into a tracking identity. The field is optional
end-to-end: a client that does not want the server to hold even this simply
never declares one.
GET actionExample request:
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world/meta HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
Example success response:
HTTP/1.1 200 OK
Content-type: application/json
{
"contentType": "application/json",
"size": 16,
"createdAt": "2026-06-10T09:12:00Z",
"updatedAt": "2026-06-12T13:25:00Z",
"createdBy": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"writerId": "z6fVXHKn8PdQm2Rt",
"custom": {
"name": "Hello World greeting",
"tags": { "project": "demo", "status": "draft" }
}
}
Errors (see F. Error Type Registry for canonical examples):
A PUT to the /meta endpoint is a full replacement of the Metadata
object's custom object: the stored custom object is replaced by the one
in the request body, so any user-writable property omitted from it is
cleared (and a request body with no custom property clears them all).
Two other top-level members are also writable, each with its own omission
behavior. A request MAY carry epoch to set the Resource's key-epoch
stamp; a request that omits it preserves the stored stamp, because the
stamp describes the content write, not the metadata write (see
C.4 Key Epochs). A request MAY carry writerId to declare the writing
agent's attribution label; a request that omits it clears the stored label
(see 11.8.1 Writer attribution: writerId and createdBy). The label is cleared rather than
preserved because a metadata write is itself a revision, and on an
encrypted Collection it replaces the custom envelope wholesale, so
keeping a previous writer's label would misattribute it.
Server-managed properties are not affected by this operation; a server MUST
ignore any top-level properties other than custom, epoch, and writerId
present in the request body (so that a client may read the Metadata object,
modify it, and PUT it back without first stripping the server-managed
properties).
Unlike the 11.6 Update (or Create By Id) Resource Operation, a PUT to
/meta does not create anything: a Metadata object cannot exist apart
from its Resource, so a PUT to the /meta path of a nonexistent Resource
returns a not-found (404) error.
PUT action204 success responseExample request:
PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/hello-world/meta HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...
{
"custom": {
"name": "Hello World greeting",
"tags": { "project": "demo", "status": "draft" }
}
}
Example success response:
HTTP/1.1 204 No Content
Errors (see F. Error Type Registry for canonical examples):
custom object (or a property within it) does not have the shape
described in 11.8 Resource Metadata Data Model, or a top-level epoch
(see C.4 Key Epochs) or writerId (see 11.8.1 Writer attribution: writerId and createdBy)
member is present but is not a non-empty string.chunked-streams token in its Backend description (see
14.1 Backend Data Model). A server whose backends do not advertise
chunked-streams MAY omit these endpoints entirely and remain conformant; see
2.4 Scope and Conformance Profiles.
A Resource MAY carry an ordered set of chunks: opaque byte sequences, each
addressed by a non-negative integer index under the Resource's reserved chunks
sub-path. Chunks let a client store a representation larger than a single request
(or a single encryption envelope) can carry, without the server ever parsing or
reassembling anything. The server treats each chunk exactly like a binary
Resource representation -- stored bytes plus a content type (see
11.3 Content Types and Representations) -- and framing and reassembly
(including any client-side encryption) are entirely the client's concern. The
server never concatenates a Resource's chunks, and reading the parent Resource's
own content (see 11.5 Read Resource Operation) returns only that content, not
its chunks; the chunk set is discovered and read through the endpoints below.
Chunks are the substrate the E. EDV-over-WAS Profile v0.1 uses to store a large or streamed encrypted document, but the mechanism itself is scheme-agnostic: the bytes of a chunk are opaque to the server whether they are plaintext, ciphertext, or anything else.
A single chunk is addressed in member form (no trailing slash), and a Resource's chunk set is listed in container form (trailing slash), following the trailing-slash convention in 2.2 Reading This Document:
PUT / GET / HEAD / DELETE
/space/{space_id}/{collection_id}/{resource_id}/chunks/{index} -- store,
read, head, and delete a single chunk.GET /space/{space_id}/{collection_id}/{resource_id}/chunks/ -- list the
Resource's chunks.As elsewhere, a request to the non-canonical variant of either form is
redirected to the canonical one with 308 Permanent Redirect (which, unlike a
302, requires the client to replay the same method and body): a GET of the
member-form chunks container without its trailing slash redirects to the
trailing-slash form, and a member PUT (or other member method) carrying a
trailing slash redirects to the no-slash form.
The {index} path segment MUST be a canonical non-negative decimal integer: a
single 0, or a digit run with no leading zero, no sign, and no non-digit
characters (so 0, 1, 42 are valid; 01, +1, -1, 1e3, 1.0 are
not). A server MUST reject a non-canonical index with an invalid-id (400)
error. Requiring a canonical spelling keeps each chunk addressable at exactly
one URL.
PUT action, whose invocationTarget is the
chunk's own full URL (see 12.7 Chunk authorization).{index}: a write replaces any chunk already stored
there. Indexes need not be written contiguously or in order.204 success response carrying the chunk's ETag.The request body is raw bytes under any Content-Type. The server MUST NOT
parse or validate a chunk body. This holds even for a Collection that declares
an encryption descriptor (see C. Encryption Scheme Registry): the
scheme's envelope validation applies to a Resource's own content, not to its
chunks, because the chunks of an encrypted stream are ciphertext fragments, not
envelope documents. The parent Resource MUST already exist; a PUT to a chunk
of a Resource that does not exist is rejected with not-found (404), so a
chunk can never be orphaned. The Digest request-body-integrity requirement
(see 5.1.4 Request Body Integrity (Digest Header)) applies per request -- that
is, per chunk. The backend's maxUploadBytes cap and quota accounting apply to
a chunk write exactly as they do to a Resource write (see 15. Quotas).
Each chunk carries its own strong ETag validator, independent of the parent
Resource's and of the other chunks'. The If-Match / If-None-Match write
preconditions of 7.2 Conditional Requests apply per chunk, against that
validator, on a backend that advertises conditional-writes.
Example request (storing chunk 0 as raw bytes):
PUT /space/81246131-69a4-45ab-9bff-9c946b59cf2e/backups/bigfile/chunks/0 HTTP/1.1
Host: example.com
Content-Type: application/octet-stream
Digest: mh=uEiCPO-qYr-z0GYV5F75-N1l8Rhjv4xIkKZsnbTZeZ7emSA
Authorization: ...
...raw chunk bytes...
Example success response:
HTTP/1.1 204 No Content
ETag: "1"
Errors (see F. Error Type Registry for canonical examples):
{index} segment is not a canonical non-negative
decimal integer (see 12.1 The chunk address).maxUploadBytes
constraint (see 15. Quotas).If-Match /
If-None-Match precondition evaluated false against the chunk's own ETag, on
a backend that advertises conditional-writes (see
7.2 Conditional Requests).A read returns the chunk's stored bytes, verbatim, with the content type they
were stored under, and the chunk's ETag. A HEAD on the same address returns
those same headers -- the response Content-Type and Content-Length are read
from the chunk's stored metadata, so the byte stream is never opened -- with no
body, mirroring the Resource HEAD variant in
11.3 Content Types and Representations. Authorization for a chunk read (and
HEAD) is capability-or-policy and resolves at the parent Resource's access
level (see 12.7 Chunk authorization).
Example request:
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/backups/bigfile/chunks/0 HTTP/1.1
Host: example.com
Authorization: ...
Example success response:
HTTP/1.1 200 OK
Content-Type: application/octet-stream
ETag: "1"
...raw chunk bytes...
Errors (see F. Error Type Registry for canonical examples):
{index} segment is not canonical (see
12.1 The chunk address).{index} (the parent Resource may
exist but have no chunk there), or the caller has missing or insufficient
authorization; per 6. Error Handling the two are indistinguishable.{index} and returns a 204 success response.If-Match precondition of 7.2 Conditional Requests against the
chunk's own ETag on a conditional-writes backend.Unlike the 11.7 Delete Resource Operation, deleting a chunk is not
idempotent: a DELETE of an absent chunk is rejected with not-found
(404), not a 204. This is deliberate -- a client reassembling a chunked
representation must be able to distinguish a chunk that is gone from one that
was never written, which an idempotent delete would erase.
Example request:
DELETE /space/81246131-69a4-45ab-9bff-9c946b59cf2e/backups/bigfile/chunks/0 HTTP/1.1
Host: example.com
Authorization: ...
Example success response:
HTTP/1.1 204 No Content
Errors (see F. Error Type Registry for canonical examples):
{index} segment is not canonical.{index} (see above), or the
caller has missing or insufficient authorization; per 6. Error Handling the
two are indistinguishable.If-Match precondition evaluated false
against the chunk's ETag, on a conditional-writes backend.The container form lists a Resource's stored chunks. Because the server never
reassembles a chunked Resource, this listing is the discovery mechanism: a
reader learns the chunk set here -- how many chunks exist and each one's index,
size, and content type -- and then reads indexes 0 through count - 1 itself.
Authorization is capability-or-policy against the chunks/ container URL,
resolving at the parent Resource's access level (see 12.7 Chunk authorization).
The response is an application/json object:
resourceId -- the parent Resource's id.count -- the number of stored chunks.chunks -- an array, in ascending index order, of one entry per stored
chunk:index -- the chunk's non-negative integer index.size -- the length in bytes of the stored chunk.contentType -- the content type the chunk was stored under.version (optional) -- the chunk's monotonic version, the integer from
which its strong ETag is derived (the ETag is this integer, quoted).
Present only on a backend that advertises conditional-writes and therefore
tracks a per-chunk version; absent otherwise.The parent Resource MUST exist for its chunk container to: a listing under an
absent Resource is a not-found (404). A Resource that exists but has no
chunks lists as count 0 with an empty chunks array.
Example request:
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/backups/bigfile/chunks/ HTTP/1.1
Host: example.com
Accept: application/json
Authorization: ...
Example success response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"resourceId": "bigfile",
"count": 3,
"chunks": [
{ "index": 0, "size": 1048576, "contentType": "application/octet-stream", "version": 1 },
{ "index": 1, "size": 1048576, "contentType": "application/octet-stream", "version": 1 },
{ "index": 2, "size": 524288, "contentType": "application/octet-stream", "version": 1 }
]
}
Errors (see F. Error Type Registry for canonical examples):
A Resource's chunks are bound to the Resource. Deleting the parent Resource (see
11.7 Delete Resource Operation) MUST cascade-delete all of its chunks; there is
no way to leave chunks behind a deleted Resource. A Resource's chunks are carried
alongside its content by a Space export and restored by the matching import (the
export reserved segment; see B. Reserved Path Segment Registry).
Chunk writes and deletes are invisible to the changes query profile (see
D.1 changes profile): storing or deleting a chunk affects only the
chunk's own ETag validator and MUST NOT advance the parent Resource's
position in the feed, and the feed enumerates Resources only, never chunks. A
client replicating a chunked Resource MUST therefore finish the write by
updating the parent Resource's own content (its manifest; see
11.6 Update (or Create By Id) Resource Operation) after its chunks are stored:
that final Resource write is what surfaces the change to replicating readers.
A client that mutates a Resource purely through its chunks never appears on
the feed.
Linksets (from [RFC9264]) serve as the main feature detection and extension
mechanism. They can be discovered, via the linkset property, from the following:
The space linkset resource (one of the B.1 Space-level reserved endpoints),
located at /space/{space_id}/linkset contains a set of links to auxiliary
resources and extension points:
policy relation
(https://wallet.storage/spec#policy) - A link to the
/space/{space_id}/policy resource, which contains a set of links to access
control policy documents.backends-available
(https://wallet.storage/spec#backends-available) - A link to the
/space/{space_id}/backends "Backends Available" resource.quotas relation
(https://wallet.storage/spec#quotas) -
A link to the /space/{space_id}/quotas per-backend storage quota report
(see 15. Quotas)./space/{space_id}/query - Reserved for cross-space query operations.Example space linkset resource request and response:
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/linkset HTTP/1.1
Accept: application/linkset+json
Authorization: ...
Response:
HTTP/1.1 200 OK
Content-type: application/linkset+json
{
"linkset": [
{
"anchor": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e",
"https://wallet.storage/spec#policy": [
{
"href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/policy",
"type": "application/json"
}
],
"https://wallet.storage/spec#backends-available": [
{
"href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/backends",
"type": "application/json"
}
],
"https://wallet.storage/spec#quotas": [
{
"href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/quotas",
"type": "application/json"
}
]
}
]
}
The collection linkset resource (one of the B.2 Collection-level reserved endpoints),
located at /space/{space_id}/{collection_id}/linkset contains a set of links
to auxiliary resources and extension points:
https://wallet.storage/spec#policy) - A link to the
/space/{space_id}/{collection_id}/policy resource, which contains a set of links
to access control policy documents.backend relation
(https://wallet.storage/spec#backend) - A link to the
detailed /space/{space_id}/{collection_id}/backend "Backend Selected for this
collection" resource.quota relation
(https://wallet.storage/spec#quota) - A
link to the /space/{space_id}/{collection_id}/quota storage quota report
for this collection (see 15. Quotas)./space/{space_id}/{collection_id}/query - (Optional) Query operations within
a collection, discriminated by a request-body profile (see
D. Query Profile Registry).Example collection linkset resource request and response:
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/linkset HTTP/1.1
Accept: application/linkset+json
Authorization: ...
Response:
HTTP/1.1 200 OK
Content-type: application/linkset+json
{
"linkset": [
{
"anchor": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages",
"https://wallet.storage/spec#policy": [
{
"href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/policy",
"type": "application/json"
}
],
"https://wallet.storage/spec#backend": [
{
"href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/backend",
"type": "application/json"
}
],
"https://wallet.storage/spec#quota": [
{
"href": "/space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/quota",
"type": "application/json"
}
]
}
]
}
Backends are an optional infrastructure concern that is orthogonal to the hierarchical Spaces Repository > Space > Collection > Resource storage model. They exist to serve advanced use cases that need fine-grained control over storage configurations.
Available backends are registered on the Space level, as a combination of server-side configuration and client-side "Bring Your Own Storage" registration.
For example, on the server configuration side, a given server might support several backends -- a file system default backend, an EDV encrypted backend, and a PostgreSQL database backend. And on the client side, a user might register an external storage provider by connecting to their Dropbox account.
When a Collection is created, the client can optionally specify the preferred
backend for that Collection. If no preferred backend is specified, one is
assigned by the server (usually the default backend).
An implementer or client of a given server can omit the backend property when
creating a Collection. By default, if not specified, all Collections are
assigned the default backend.
A Backend description object advertises a backend's identity and features so that clients can select a suitable backend for each Collection (and so that storage management UIs can present meaningful choices to users).
Backend description properties:
id (required) - A unique backend identifier (within a given space). The id
default is conventionally used for the server-assigned default backend.
See 4. Identifiers for additional constraints.name (optional) - An arbitrary human-readable name for the backend. Does
not have to be unique.managedBy (optional) - Who operates the backend. One of:server - configured and operated server-side (for example, the server's
filesystem or database backends). This is the default if unspecified.external - a "Bring Your Own Storage" provider registered by the client
(for example, a connected Dropbox account).storageMode (optional) - An array of storage modalities the backend
supports. This specification defines two values: document (structured JSON
resources) and blob (opaque binary byte streams). Advertising modalities up
front prevents mismatches, such as attempting to store a multi-gigabyte
binary file on a JSON-only document store. If unspecified, clients SHOULD
assume both modalities are supported.persistence (optional) - Either durable (the engine stores data on
persistent media, e.g. disk, and it survives a restart) or volatile (the
engine stores data in memory, e.g. a RAM-backed cache tier, and data may not
survive a restart). Defaults to durable. Note that this is a technical
property of the storage engine, deliberately distinct from any administrative
data-retention rules (see the editor's note on lifecycle configuration below).features (optional) - An array of feature tokens advertising optional
server affordances the backend provides beyond the baseline read/write API,
so that clients can gate behavior on what the backend actually does. The
vocabulary is open and additive: a client MUST ignore tokens it does not
recognize, and MUST treat an absent token (or an absent features array) as
"not supported" rather than assuming a default. Tokens this specification
defines:conditional-writes - the backend enforces If-Match / If-None-Match
write preconditions (see 7.2 Conditional Requests).changes-query - the backend serves the changes profile of the query
endpoint (see D. Query Profile Registry).blinded-index-query - the backend serves the blinded-index profile of the
query endpoint (see D. Query Profile Registry).chunked-streams - the backend supports chunk addressing for large blobs
(see 12. Chunked Resources).key-epochs - the backend persists the Resource epoch stamp, serves it
on metadata/listing/feed reads, and enforces conditional Collection
Description writes (ETag / If-Match), i.e. the server affordances
C.4 Key Epochs requires. Clients gate recipient-management UX on this
token.Each token names something the server must actively do. Note that
client-side encryption is deliberately not a backend feature: an encrypted
document is opaque client-encrypted JSON that any document-capable backend
stores faithfully with no server cooperation, and whether a given Collection
is encrypted varies per Collection on the very same backend. Encryption is
therefore a property of a Collection's data (signaled at the Collection level
and held in the client's keys), not a capability of the backend. Concretely,
this signal is the Collection's optional encryption descriptor (see
10.1 Collection Data Model): a non-secret declaration any authorized reader
discovers from the Collection Description, while the keys stay in the client.
A backend that advertises conditional-writes takes on the server-side half of
the client contract in 7.2 Conditional Requests. It MAY derive the ETag
from an internal monotonic version counter (such as an Encrypted Data Vault
document sequence); the validator is opaque to clients. It MUST evaluate a
write's If-Match / If-None-Match precondition atomically with the write
(for example, under a per-Resource lock), so that two concurrent writers cannot
both observe the same prior version and both succeed. An in-process
per-Resource lock satisfies this for a single-instance server only; a
horizontally-scaled deployment needs to coordinate the check-and-write across
instances (e.g. an atomic compare-and-swap on the stored version or a shared
lock).
The list of backends registered on a Space is discoverable via the
backends-available relation in the linkset (see 13.1 Space Linkset).
Example request:
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/backends HTTP/1.1
Accept: application/json
Authorization: ...
Example success response:
HTTP/1.1 200 OK
Content-type: application/json
[
{
"id": "default",
"name": "Server Filesystem",
"managedBy": "server",
"storageMode": ["document", "blob"],
"persistence": "durable"
},
{
"id": "dropbox",
"name": "Dropbox",
"managedBy": "external",
"storageMode": ["blob"],
"persistence": "durable"
},
{
"id": "edv",
"name": "Encrypted Data Vault",
"managedBy": "server",
"storageMode": ["document", "blob"],
"persistence": "durable",
"features": ["conditional-writes", "blinded-index-query", "chunked-streams"]
}
]
Each collection has an optional backend property that is set during its creation
(see 10.1 Collection Data Model). If not specified, it is assumed to have the
id of default. The selected backend is discoverable via the
backend relation in the collection's linkset (see
13.2 Collection Linkset).
backend property is
expected to generalize to one or more _replicas_ per Collection, each replica
hosted on a backend, with the current single-backend configuration as the
degenerate one-replica case. (Collections do not "live" on a single backend;
backends are infrastructure endpoints, not a fourth tier of the storage
hierarchy.) That generalization will bring with it:active vs available) -- this
specification will define the vocabulary for declaring replica topology;
the logic for selecting the active replica (based on latency, battery,
network status) belongs to the client SDK.synced, syncing, stale);
the exact state machine, including how "cold boots" of volatile backends
are surfaced, is to be determined.storageMode declaration, validated against each
replica's backend at creation time.If-Match ETags) for updates to replica topology,
building on the per-Resource conditional-writes mechanism (see
7.2 Conditional Requests) generalized to multi-replica writes.persistence property (a technical attribute of the storage engine)
and from access-control policy (who may act on the data). The property
naming is to be determined, precisely to avoid overloading the term "policy".
Quota reporting and enforcement are optional, and support is backend-dependent. The quota API serves two distinct consumers:
include query
parameter.Quota endpoints follow the same maximum-privacy invariant as the rest of this specification (see 6. Error Handling): a caller not authorized to read a Space's quota report MUST receive a not-found (404) error, never a 403.
Returns the storage report for a Space, grouped by backend, and is discoverable
via the quotas relation in the Space's linkset (see 13.1 Space Linkset).
Each entry in the backends array carries:
id, name, managedBy - identifying properties from the corresponding
Backend description object (see 14.1 Backend Data Model).state - the backend's current condition, one of: ok, near-limit,
over-quota, or unreachable.usageBytes - total bytes consumed on this backend by this Space.limit - an object with capacityBytes and an isUnlimited boolean (when
isUnlimited is true, capacityBytes MAY be omitted).constraints (optional) - operational constraints such as
maxUploadBytes, the largest single upload the backend accepts.restrictedActions - an array of actions (uppercase HTTP verbs, the
same vocabulary as the WAS Authorization Profile, see
5.1.5 Authorization Actions and the Root Capability) currently unavailable
on this backend. For example, a full backend reports ["POST", "PUT"]
while still permitting reads and deletes.measuredAt - when the usage numbers were measured. For external
("Bring Your Own Storage") backends, the server proxies the provider's
reported numbers, which may be cached or stale; measuredAt lets clients
judge freshness independently of the report's top-level respondedAt.Example request:
GET /space/81246131-69a4-45ab-9bff-9c946b59cf2e/quotas HTTP/1.1
Accept: application/json
Authorization: ...
Example success response:
HTTP/1.1 200 OK
Content-type: application/json
{
"respondedAt": "2026-06-12T13:25:00Z",
"backends": [
{
"id": "default",
"name": "Server Filesystem",
"managedBy": "server",
"state": "ok",
"usageBytes": 524288000,
"limit": { "capacityBytes": 10737418240, "isUnlimited": false },
"restrictedActions": [],
"measuredAt": "2026-06-12T13:25:00Z"
},
{
"id": "dropbox",
"name": "Dropbox",
"managedBy": "external",
"state": "near-limit",
"usageBytes": 314572800,
"limit": { "capacityBytes": 367001600, "isUnlimited": false },
"constraints": { "maxUploadBytes": 157286400 },
"restrictedActions": [],
"measuredAt": "2026-06-12T13:24:45Z"
}
]
}
With the include=collections query parameter, each backend entry
additionally carries a usageByCollection array, giving storage-manager
applications a full breakdown in a single request while keeping the hot-path
payload lean:
HTTP/1.1 200 OK
Content-type: application/json
{
"respondedAt": "2026-06-12T13:25:00Z",
"backends": [
{
"id": "default",
"name": "Server Filesystem",
"managedBy": "server",
"state": "ok",
"usageBytes": 524288000,
"limit": { "capacityBytes": 10737418240, "isUnlimited": false },
"restrictedActions": [],
"measuredAt": "2026-06-12T13:25:00Z",
"usageByCollection": [
{ "id": "credentials", "usageBytes": 419430400 },
{ "id": "inbox", "usageBytes": 104857600 }
]
}
]
}
Returns the storage report for a single Collection, scoped to that
Collection's backend (the entry has the same shape as a backends array
entry, with usageBytes reflecting only this Collection's consumption). It is
discoverable via the quota relation in the Collection's linkset (see
13.2 Collection Linkset).
Not all backends support per-collection accounting. Where unsupported, the server returns an unsupported-operation (501) error.
Errors (see F. Error Type Registry for canonical examples):
This appendix is normative. It defines the full pagination profile summarized in 7.3 Paginated List Responses.
The list operations -- 8.2 List Spaces Operation, 9.5 List All Collections operation,
and 10.5 List Collection operation -- return a collection of items in the
common envelope (url, totalItems, items). A Space or Collection may hold
more items than is practical to return in a single response, so servers MAY
paginate these responses, returning one page of items at a time. Pagination
is OPTIONAL: a server that returns every item in a single response (as the
examples in those sections show) is conformant, and a client MUST be prepared
for either behavior.
This profile uses cursor-based (also called "keyset") pagination rather than numeric offsets. A cursor identifies a position in a stably ordered result set, so paging stays correct and cheap even as items are concurrently added or removed, and at any depth into a large collection.
A paginated list operation MUST return items in a stable total order: an
ordering that is deterministic and in which no two items compare equal. The
default order is ascending by item id (which is unique within its parent, see
4. Identifiers). A server MAY offer additional orderings, but every order it
supports for pagination MUST be stable and total -- if the primary sort key is
not unique (for example, a timestamp), the server MUST break ties on a unique key
(such as id) so that the combined order is total. This stable order is what a
cursor seeks within; an unstable order cannot be paginated correctly.
A client requests pagination with two OPTIONAL query parameters:
limit - the maximum number of items the client wants in the page, a
positive integer. A server applies its own default when limit is absent, and
MAY clamp an oversized limit down to a server maximum rather than rejecting
the request. A server MAY return fewer items than limit (including zero) and
still indicate more pages follow; clients MUST NOT treat a short page as the
end of the list (see next, below).cursor - an opaque token, obtained from a prior page's next value (see
below), naming the position to continue from. A client MUST treat the cursor as
opaque: it MUST NOT construct, parse, or modify it, and MUST NOT carry a cursor
from one collection, ordering, or server to another. A server encodes whatever
it needs into the cursor to resume the ordered scan (typically the sort-key
value of the last item already returned).The first page is requested with no cursor (a bare limit, or neither
parameter).
A paginated response carries the usual envelope, plus a next member when more
items may follow:
next - a URL the client dereferences (with the same authorization) to
retrieve the following page. The server bakes the appropriate cursor (and
any limit) into this URL, so the client follows it without constructing query
parameters itself. next is present if and only if more items may follow;
its absence marks the last page. This presence/absence is the authoritative
end-of-list signal.totalItems - when present, the total number of items in the entire
collection, not the number in the current page (the page count is simply the
length of items). Computing an exact total can be expensive at scale, so a
paginating server MAY omit totalItems. A client MUST NOT infer the number of
pages, or whether more pages exist, from totalItems; only next is
authoritative.A client consumes a multi-page list by following next from each response until
a response omits it. A server SHOULD ensure that an item present throughout the
traversal appears exactly once across the pages; an item added or removed
concurrently with the traversal MAY or MAY NOT appear. Snapshot consistency
(a paginated traversal observing a single point in time) is permitted but not
required. A server MAY encode a snapshot identifier into the cursor to provide
such consistency.
A cursor that is malformed, or that a server can no longer honor (for example,
an expired snapshot), is rejected with an invalid-cursor (400) error. As
with other request-validation failures, a server MUST verify the caller's
authorization for the target before validating the cursor, so the error is
only ever observed by a caller already authorized to list that target; an
under-authorized caller receives the merged not-found (404) instead, per
6. Error Handling.
This appendix is normative.
The following path segments represent reserved API endpoints for 9. Spaces
level operations. Usually, the path segment following the /space/{space_id}/ prefix
is the id of a Collection. The list of reserved endpoints below means that
collections ids MUST NOT collide with the corresponding reserved segments.
| Reserved API Endpoint | Reserved segment | Purpose |
|---|---|---|
/space/{space_id}/policy |
policy |
Access control policy |
/space/{space_id}/backends |
backends |
Storage backends available |
/space/{space_id}/collections |
collections |
List and create collections |
/space/{space_id}/export |
export |
Export (download) space contents |
/space/{space_id}/import |
import |
Import (upload) a space export archive |
/space/{space_id}/linkset |
linkset |
Links to auxiliary resources |
/space/{space_id}/query |
query |
Reserved for cross-collection queries |
/space/{space_id}/quotas |
quotas |
Reserved for per-backend quota report |
If a client attempts to create a collection with an id that collides with a
reserved segment list above, the server MUST return a 409 Conflict error.
The following path segments represent reserved API endpoints for 10. Collections
level operations. Usually, the path segment following the
/space/{space_id}/{collection_id}/ prefix is the id of a Resource. The list of
reserved endpoints below means that resource ids MUST NOT collide with the
corresponding reserved segments.
| Reserved API Endpoint | Reserved segment | Purpose |
|---|---|---|
/space/{space_id}/{collection_id}/policy |
policy |
Access control policy |
/space/{space_id}/{collection_id}/backend |
backend |
Storage backend selected |
/space/{space_id}/{collection_id}/linkset |
linkset |
Links to auxiliary resources |
/space/{space_id}/{collection_id}/meta |
meta |
Collection metadata (server-managed and user-writable); see 10.7 Collection Metadata Data Model |
/space/{space_id}/{collection_id}/query |
query |
Query resources within a collection (see D. Query Profile Registry) |
/space/{space_id}/{collection_id}/quota |
quota |
Storage quota report for collection |
If a client attempts to create a resource with an id that collides with a
reserved segment list above, the server MUST return a 409 Conflict error.
The following path segments represent reserved API endpoints for Resource level operations.
| Reserved API Endpoint | Reserved segment | Purpose |
|---|---|---|
/space/{space_id}/{collection_id}/{resource_id}/meta |
meta |
Resource metadata (server-managed and user-writable) |
/space/{space_id}/{collection_id}/{resource_id}/chunks |
chunks |
Chunk addressing for a chunked Resource (see 12. Chunked Resources) |
Unlike the Space- and Collection-level reserved segments, which occupy the id
position one level down and so constrain Collection and Resource id choice,
these sit below the Resource level -- they qualify a {resource_id} -- and
impose no constraint of their own on id choice at any level. Note meta's
distinct role at each level: the Collection-level row occupies the
{resource_id} position, so meta is a reserved Resource id (by that row,
not this table), while the Resource-level /meta and /chunks segments sit
one level lower and shadow nothing -- a Resource whose own id is chunks is
unaffected.
This appendix is normative.
A Collection's optional encryption descriptor (see 10.1 Collection Data Model)
names a client-side encryption scheme and a version of that scheme's wire
format -- a positive integer, starting at 1 for each scheme and incremented
per registered revision (an absent version means 1). This registry maps
each scheme/version pair to the wire format the
server can expect for Resources in such a Collection, so
that a server can hold the collection's fail-closed guarantee
structurally, by validating the shape of what is written, rather than
relying on every client to encrypt correctly. The server never holds key
material and never decrypts; it validates only the non-secret envelope
structure, so this enforcement neither requires nor weakens confidentiality.
scheme |
version |
Media type | Envelope profile | Reference |
|---|---|---|---|---|
edv |
1 |
application/json |
An Encrypted Data Vault Encrypted Document: a JSON object whose jwe member is a JWE in JSON Serialization ([RFC7516] §7.2) -- a JSON object carrying at least a ciphertext member and either a recipients array (general serialization) or a top-level encrypted_key/protected (flattened serialization). The document MAY also carry EDV bookkeeping members (id, sequence, indexed); these are opaque to the server. |
Encrypted Data Vaults |
A server that recognizes the scheme/version pair declared by a
Collection's encryption descriptor MUST validate the body of every Resource
content write (POST or PUT) into that Collection against that pair's
envelope profile, and MUST reject a non-conforming body -- or a body sent under a
Content-Type other than the scheme's registered media type -- with an
encryption-scheme-mismatch error.
This is the structural fail-closed guarantee: a client that has not encrypted the body, whether through a bug or an omission, cannot store server-visible plaintext in an encrypted Collection. The check is purely structural; a server MUST NOT attempt decryption and MUST NOT inspect the envelope's contents.
The rule applies to a Resource's stored representation and, on an encrypted
Collection, to the user-writable custom object of its
11.8 Resource Metadata Data Model (see the encrypted-Collection note there).
It does not apply to the rest of the server-managed API documents -- Collection
Descriptions, the server-managed top-level Metadata properties, policy
documents, linksets -- which remain application/json regardless of the
Collection's encryption status. For the Metadata object specifically, the
document itself stays a plaintext application/json object (so the server keeps
its top-level contentType, size, and timestamps); only the custom
sub-value MUST be a conforming envelope on an encrypted Collection, validated the
same fail-closed way (an encryption-scheme-mismatch on non-conformance).
As with id-conflict, a server MUST verify the caller's authorization before validating the envelope, so that encryption-scheme-mismatch is observable only to callers already authorized to write at that target; an under-authorized caller receives the merged not-found instead, and learns nothing about the Collection.
When a Collection create or update operation declares an encryption descriptor (see
10.3 Update (or Create By Id) Collection operation), a server SHOULD reject a
scheme -- or a version of a recognized scheme -- that it does not recognize
with an unsupported-encryption-scheme error, rather than storing a descriptor it
cannot enforce. This ensures that every descriptor
a server accepts is one it validates on write: "this Collection is declared
encrypted" then structurally implies "plaintext writes to it are rejected here,"
closing the gap that a silently unenforced descriptor would reopen.
A server MAY instead choose to store descriptors for schemes it does not enforce (treating the descriptor as fully opaque, per 10.1 Collection Data Model), but such a server MUST document that it provides no server-side fail-closed guarantee for those Collections, leaving the guarantee entirely to clients.
A server MAY implement the edv 1 envelope profile with a JSON Schema equivalent
to the following. The outer object MUST carry a jwe member; only the
jwe's structural members are checked; their values are opaque ciphertext and
are not interpreted. A plaintext object under application/json (one with no
valid jwe) fails this profile and is rejected with an
encryption-scheme-mismatch, preserving the fail-closed guarantee.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["jwe"],
"properties": {
"jwe": {
"type": "object",
"required": ["ciphertext"],
"properties": {
"protected": { "type": "string" },
"iv": { "type": "string" },
"ciphertext": { "type": "string" },
"tag": { "type": "string" },
"encrypted_key": { "type": "string" },
"recipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"header": { "type": "object" },
"encrypted_key": { "type": "string" }
}
}
}
},
"anyOf": [
{ "required": ["recipients"] },
{ "required": ["encrypted_key"] },
{ "required": ["protected"] }
]
}
}
}
A key epoch is one generation of a Collection's encryption key. Rather than
encrypting every Resource under a single long-lived key, writers encrypt each
Resource under whichever epoch was current when it was written; the
Collection's encryption descriptor carries the full, append-only roster of
epochs, so the roster as a whole reads as the Collection's access history. The
full client-side construction is specified in [WAS-EC]; this section defines
only what the server stores and validates.
An encrypted Collection with a single shared key set has no cryptographic notion of removing a reader: revoking the reader's capability stops the server serving it ciphertext, but the reader still holds the key. Key epochs make removal meaningful by separating two axes:
Removing a reader means doing both: revoke its capability AND rotate the epoch. Neither alone suffices.
A Collection's encryption descriptor MAY carry two additional members:
epochs - An array of epoch objects, each with:id - A unique, opaque epoch identifier (a non-empty string). The
reference client uses the did:key identifier of the epoch's public
key-agreement key, so that a stored envelope's JWE recipient kid names
its epoch directly, but servers MUST treat the value as opaque.recipients - A non-empty array of wrapped-key entries, one per reader.
Each entry reuses the JWE General Serialization recipients entry shape
verbatim ([RFC7516] section 7.2): a header object with at least a
non-empty string kid (the reader's key-agreement key identifier) and
alg (e.g. ECDH-ES+A256KW), plus the wrapped epoch key as a string
encrypted_key. This is the same shape the edv envelope profile already
validates -- one wire vocabulary for "a key wrapped to a recipient".currentEpoch - The id of the epoch new writes encrypt under. MUST name
an entry in epochs.Nothing secret appears in these fields: recipient entries hold public key identifiers and wrapped-key ciphertext only. The server never holds key material, never unwraps, and never verifies that a wrapped key is correct -- that is unverifiable without the keys, by construction.
Example descriptor:
{
"scheme": "edv",
"currentEpoch": "did:key:z6LSepoch2...#z6LSepoch2...",
"epochs": [
{
"id": "did:key:z6LSepoch1...#z6LSepoch1...",
"recipients": [
{
"header": {
"kid": "did:key:z6MkappA...#z6LSkakA...",
"alg": "ECDH-ES+A256KW",
"epk": { "kty": "OKP", "crv": "X25519", "x": "..." },
"apu": "...",
"apv": "..."
},
"encrypted_key": "base64url..."
}
]
},
{
"id": "did:key:z6LSepoch2...#z6LSepoch2...",
"recipients": [ "..." ]
}
]
}
A server that recognizes the edv scheme MUST validate the key-epoch members
when a Collection create or update supplies them, rejecting a violation with an
invalid-request-body error (and a JSON pointer to the offending member):
epochs and currentEpoch MUST appear together or not at all.epochs MUST be a non-empty array; each entry MUST have a non-empty string
id, unique across the array, and a non-empty recipients array.recipients entry MUST have a header object with non-empty string
kid and alg members, plus a string encrypted_key.currentEpoch MUST name an id present in epochs.On an update of a descriptor that already carries epochs, two further rails apply. These protect the Collection's own readers from client bugs -- a dropped epoch strands every Resource stamped with it -- and are enforceable without the server reading a single byte of what the epochs protect:
epochs is append-only. Every previously stored epoch id MUST still be
present. (Entries within an existing epoch's recipients MAY change: adding
a reader wraps existing epoch keys to it.)currentEpoch never moves backwards. The updated currentEpoch MUST
either equal the stored one or name an epoch id that was not previously
present (a newly appended epoch). This formulation is independent of array
order.Beyond these rails the server MUST NOT interpret the fields. In particular it
MUST NOT attempt to check that recipients matches any set of capability
grantees -- the two axes are deliberately independent, and keeping them in sync
is the client's job.
A reader must know which epoch key to unwrap before attempting decryption. The writer therefore declares the epoch a Resource was encrypted under, and the server stores and serves that declaration:
POST or PUT) MAY carry a Key-Epoch
request header whose value is the epoch id (a non-empty string; a present
but empty or malformed value is an invalid-request-body error). The
server MUST store the value verbatim as the Resource Metadata epoch
property. A content write without the header MUST clear any stored
stamp: the new representation's epoch is unknown, and a stale stamp is worse
than none. The value is client-declared, advisory metadata: the server MUST NOT validate it against the descriptor's epochs (a write may race a rotation)
and MUST NOT compute it.epoch member (a
sibling of custom, and like the header a non-empty string). Unlike
custom, which is a full replacement, an omitted epoch PRESERVES the
stored value -- the stamp describes the content write, not the metadata
write. It MUST NOT live inside custom: on an encrypted Collection custom
is the opaque envelope and is replaced wholesale by every metadata write.epoch member, stamping the epoch the Collection Metadata's own custom
envelope was encrypted under. Its omission rule is inverted: an omitted
epoch CLEARS the stored value, because at that level the stamp describes
the custom envelope itself, which every metadata write replaces
wholesale. See 10.7 Collection Metadata Data Model.epoch property of the
Resource Metadata object, on each item of a List Collection response, and on
each document of the changes query profile -- so a
reader walking a listing
or replicating from the feed can select its epoch key without a per-Resource
metadata fetch.The reference construction (implemented by @interop/was-client and
normatively specified in [WAS-EC]); other constructions are conformant at
this specification's level so long as stored envelopes keep satisfying the
edv envelope profile:
edv descriptor without epochs, and never seals
an envelope directly to a reader's own key-agreement key.id is the
did:key identifier of the epoch public key, so any standard did:key
resolver that supports X25519 type did:key DIDs can resolve the JWE
recipient kid of a stored envelope, and the kid itself names the epoch.ECDH-ES+A256KW, the epoch key pair as the JWE's
sole recipient). The stored envelope is the ordinary EDV Encrypted Document
shape; only the key resolution process differs from a native EDV. Each
envelope additionally binds its epoch into the JWE protected header
([WAS-EC]'s was binding), checked unconditionally on read.recipients entries wrap the 32-byte epoch secret to each
reader's own key-agreement key with ECDH-ES+A256KW (ephemeral-static
ECDH, the RFC 7518 Concat KDF, AES key wrap). A reader finds its kid in an
epoch's recipients, unwraps the epoch secret with its own key, and
reconstructs the epoch key pair. A failed unwrap is a failure.
In particular, key servers whose unwrap operation resolves a null key on
mismatch must be handled by trying the next candidate or failing with a
typed error.epoch stamp
(from metadata, the listing
item, or the feed document) as advisory pre-fetch routing: it lets a
replica select and unwrap the epoch key before fetching the envelope. The
authoritative epoch is the envelope's own -- the JWE recipient kid
names it, and the AEAD-bound epoch binding is verified against the
decrypting key ([WAS-EC]). An absent stamp therefore just means
route-after-fetch; it is never treated as "assume currentEpoch", and
there are no unstamped pre-epoch resources to tolerate.currentEpoch and stamp it via
Key-Epoch.If-Match. No rotation: adds are inexpensive, removals rotate.currentEpoch, write with If-Match;
(3) subsequent writes use the new epoch. Client libraries should not expose
a rotate-without-revoke or revoke-without-rotate implementation of "remove".hmac reference on removal would invalidate every blinded index in the
Collection. A removed reader retaining the ability to compute blinded index
terms is harmless: the server gates the blinded-index query profile behind
a capability the reader no longer holds.changes feed entry. A rekey is a Collection
Description change, not a Resource change. A replicating reader that
encounters an epoch it does not know MUST re-read the Collection
Description.This appendix is normative.
A collection MAY expose an OPTIONAL query endpoint at
POST /space/{space_id}/{collection_id}/query. The endpoint is a single URL that
serves several distinct query dialects; a request selects one by carrying a
profile discriminator in its body. This registry maps each profile token to
the request and response wire shape of that dialect.
The request body is an application/json object with a REQUIRED profile string
member. All other members are profile-specific. A server that recognizes the
profile answers with 200 OK on success and a profile-specific response body.
A server that does not serve a given profile, whether because the server does
not implement it at all, or because the Collection's backend lacks the
capability (see 14.1 Backend Data Model), MUST respond with an
unsupported-operation (501) error.
The profile vocabulary is open and additive, mirroring the backend
features vocabulary (see 14.1 Backend Data Model): this specification defines
the tokens below, and future profiles register additional tokens. A server MUST
treat a profile it does not recognize the same as one it does not serve, with
unsupported-operation.
Authorization. The query endpoint requires read authorization (a capability
or a policy grant, as for any read); the invoked action is POST. The
capability's invocationTarget is the bare /query URL. Every query parameter
travels in the request body, which is signed and covered by the request Digest
(see 5.1.4 Request Body Integrity (Digest Header)), so no query-string
attenuation is involved, and a capability that authorizes the /query target
authorizes any query body it signs. As with every other operation, a server MUST
verify authorization before validating the request body. An under-authorized
caller therefore receives the privacy-preserving not-found (404, see
6. Error Handling) and never learns whether its query body was well-formed,
nor whether the target Collection exists.
profile |
Summary | Defining subsection |
|---|---|---|
changes |
Ordered replication change feed (a resumable pull loop) | D.1 changes profile |
blinded-index |
Matching over client-computed blinded (HMAC'd) attributes | D.2 blinded-index profile |
The changes profile serves an ordered, resumable feed of the content and
metadata changes in a collection, so that an offline-first replication client
can pull a Collection's state incrementally and keep a local replica in sync. A
server that serves this profile SHOULD advertise the changes-query backend
feature (see 14.1 Backend Data Model).
Request body:
{
"profile": "changes",
"checkpoint": { "id": "<resourceId>", "updatedAt": "<ISO-8601 timestamp>" },
"limit": 100
}
checkpoint (OPTIONAL) - the position to resume from, echoed from a prior
response (see below). When present it MUST be an object with a string id and a
string updatedAt; a malformed checkpoint is rejected with an
invalid-request-body (400) error. When absent, the feed starts at the
beginning.limit (OPTIONAL) - the requested batch size. A non-finite value, or a value
less than 1, falls back to a server default (a server default of 100 is
RECOMMENDED); a server MAY clamp limit down to an implementation maximum.Response body:
{
"documents": [
{
"id": "hello-world",
"_deleted": false,
"updatedAt": "2026-01-15T12:00:00.000Z",
"version": 1,
"metaVersion": 3,
"createdBy": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"writerId": "z6fVXHKn8PdQm2Rt",
"data": { "message": "hi" },
"custom": {
"name": "Hello World greeting",
"tags": { "project": "demo", "status": "draft" }
}
}
],
"checkpoint": { "id": "hello-world", "updatedAt": "2026-01-15T12:00:00.000Z" }
}
Each entry in documents describes one changed Resource:
id - the Resource id._deleted - a boolean; true on a tombstone (a soft-deleted Resource), false
otherwise. The underscore-prefixed member name is deliberate: it matches the
wire convention of offline-first replication clients.updatedAt - the ISO-8601 timestamp of the change; the feed's ordering key.version - a monotonic content version. This is the same validator a server
surfaces as the ETag where conditional writes are supported (see
7.2 Conditional Requests); it is always present, and a content write bumps
it.metaVersion - a monotonic metadata version, present only once metadata has
been written for the Resource, and bumped by a metadata-only edit.createdBy - the did of the Resource's creator, as defined in
11.8 Resource Metadata Data Model. A server that records createdBy SHOULD
surface it here, so that a replica learns each Resource's creator from the feed
rather than dereferencing the meta sub-resource of every Resource it pulls.
Present only when recorded; a client MUST treat an absent createdBy as "not
recorded". It is server-managed and read-only, as it is everywhere else.data - the stored JSON body verbatim. The body is nested under data (rather
than spread onto the entry) so that a non-object JSON body round-trips
faithfully. Omitted on a tombstone.custom - the user-writable metadata object (see
11.8 Resource Metadata Data Model), present only when set. On an encrypted
Collection this is the opaque client-encrypted envelope, passed through
untouched.epoch (optional) - the Resource's key-epoch stamp, mirroring the Resource
Metadata property (see C.4 Key Epochs), carried so that a replica can
select its epoch key without a metadata fetch per document. Note that a
rekey (a Collection Description change) emits no feed entry of its own: a
replica that encounters an epoch it does not know re-reads the Collection
Description.writerId (optional) - the Resource's writer-attribution label, mirroring
the Resource Metadata property (see 11.8.1 Writer attribution: writerId and createdBy). A server
that stores writerId MUST include it here, as this is the member that lets a
replica recognize its own writes echoed back (entries carrying its own
label) without fetching (and on an encrypted Collection, without decrypting)
each document, and lets replicas break same-updatedAt last-writer-wins
ties deterministically on a shared (updatedAt, writerId) key. Like
everywhere else it is advisory and never server-verified.Tombstones. A soft-deleted Resource surfaces as
{ "id", "_deleted": true, "updatedAt", "version" } with no data member;
the deletion bumps version. A tombstone retains its createdBy, where one was
recorded, so that a deletion replicates together with the attribution of the
Resource it removes. It also carries as writerId the label the deleting
request declared (see 11.8.1 Writer attribution: writerId and createdBy), if any. Since a deletion is a
revision, its attribution and tie-breaking work like any other write's.
Ordering and resumption. Entries are ordered by an ascending (updatedAt, id)
keyset. The top-level checkpoint echoes the position of the last entry in
documents, or is null when nothing changed past the supplied checkpoint
(the end of the feed). A client resumes by sending the returned checkpoint as
the checkpoint of its next request; the resulting sequence of calls is the pull
loop of a replication protocol. A metadata-only edit re-surfaces the Resource
with a bumped updatedAt and metaVersion but an unchanged version and data.
Scope. Binary (non-JSON) Resources are excluded from the changes feed; blob
replication is out of scope for this profile.
Example -- request the first batch and receive one changed document plus the checkpoint to resume from:
POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/query HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...
{ "profile": "changes", "limit": 100 }
HTTP/1.1 200 OK
Content-type: application/json
{
"documents": [
{
"id": "hello-world",
"_deleted": false,
"updatedAt": "2026-01-15T12:00:00.000Z",
"version": 1,
"createdBy": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"data": { "message": "hi" }
}
],
"checkpoint": { "id": "hello-world", "updatedAt": "2026-01-15T12:00:00.000Z" }
}
The blinded-index profile serves queries over client-computed blinded (HMAC'd)
attributes, so that a server can match encrypted documents without seeing
plaintext attribute names or values. Its semantics follow the query operation of
the Encrypted Data Vaults specification
(the same specification the C. Encryption Scheme Registry references for the
edv envelope). A server that serves this profile SHOULD advertise the
blinded-index-query backend feature (see 14.1 Backend Data Model).
Documents opt in by carrying the EDV indexed member: an array of entries of the
shape { "hmac": { "id", "type" }, "sequence", "attributes": [ { "name", "value", "unique"? } ] }, in which each name and value is a blinded (HMAC'd)
token computed by the client. A server stores the indexed member and matches
against it, but cannot invert it back to the plaintext attribute.
Request body:
{
"profile": "blinded-index",
"index": "<HMAC key id>",
"equals": [ { "<blindedName>": "<blindedValue>" } ],
"has": [ "<blindedName>" ],
"count": false,
"limit": 10,
"cursor": "<opaque token>"
}
index (REQUIRED) - a non-empty string naming which blinded index (the HMAC
key id) to search. Matching is scoped to entries under this index.equals or has MUST be present; supplying neither, or both,
is an invalid-request-body (400) error.equals - a non-empty array of objects, each mapping a blinded attribute name
to a blinded attribute value (all strings). The array is a disjunction (OR
across its elements); the pairs within one element are a conjunction (AND). An
empty object element matches nothing.has - a non-empty array of blinded attribute name strings; matches every
document that carries every named attribute under the index, regardless of
value.count (OPTIONAL) - a boolean. When true, the response is exactly
{ "count": <number> } (the number of matching documents) rather than a
document page.limit (OPTIONAL) - the requested page size, handled leniently as for the
D.1 changes profile profile (a non-finite or < 1 value falls back to
a server default; a server MAY clamp to a maximum).cursor (OPTIONAL) - an opaque pagination token from a prior response (see
below). A malformed cursor is rejected with an invalid-cursor (400)
error.Response body (a document page):
{ "documents": [ { } ], "hasMore": false, "cursor": "<opaque token>" }
documents - the matching stored documents, verbatim (an encrypted envelope
passes through untouched), in ascending Resource id order.hasMore - a boolean, true when more matching documents follow the page.cursor - an opaque continuation token, present if and only if hasMore is
true. A client resumes by echoing it as the cursor of its next request. As
with pagination cursors elsewhere (see A. Pagination), a client MUST treat
the token as opaque and MUST NOT construct, parse, or modify it.Cursor pagination is a deliberate WAS extension over the Encrypted Data Vaults
query operation, which offers only limit together with a hasMore flag and no
means to fetch the next page. A server that also implements the EDV query
operation directly may expose limit/hasMore there while offering the
cursor continuation only through this profile.
Example -- match documents whose blinded index-2 attribute equals a blinded
value:
POST /space/81246131-69a4-45ab-9bff-9c946b59cf2e/messages/query HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: ...
{
"profile": "blinded-index",
"index": "z1A...hmacKeyId",
"equals": [ { "aBlindedName": "aBlindedValue" } ],
"limit": 10
}
HTTP/1.1 200 OK
Content-type: application/json
{
"documents": [
{ "id": "urn:uuid:...", "sequence": 0, "jwe": { "ciphertext": "..." } }
],
"hasMore": false
}
An indexed attribute entry MAY carry "unique": true, marking its
(hmac id, name, value) triple as a uniqueness claim within the Collection. A
server MUST reject a Resource content write that would claim a triple already
claimed with unique: true by a different Resource in the same collection,
with an id-conflict (409) error. A document re-asserting its own existing
claim never self-conflicts, and the same triple carried without unique
coexists freely.
This uniqueness is enforced at write time, not at query time. As with the
id-conflict rule elsewhere in this specification (see the id-conflict privacy
note in F. Error Type Registry), a server MUST verify the caller's
authorization to write at the target before performing this check, so that the
existence-revealing 409 is observable only to a caller already authorized to
write; an under-authorized caller receives the merged not-found instead. The
check MUST be atomic with the write, so that two concurrent writers cannot both
claim the same triple.
This appendix is normative for clients that claim conformance to it.
An Encrypted Data Vault stores JWE-encrypted documents that its server can query by blinded index but never decrypt. A client can realize all of that behavior on a plain WAS server, because a WAS Resource is an opaque byte store and WAS already carries the pieces an EDV needs. This profile fixes the mapping so that independent clients interoperate over the same encrypted Collection:
| EDV concept | WAS realization |
|---|---|
| Vault | collection |
| Document | Resource (the EDV envelope stored as its content) |
Document sequence |
conditional writes (If-Match / If-None-Match; see 7.2 Conditional Requests) |
| Blinded index + query | the blinded-index query profile (see D.2 blinded-index profile) |
| Stream chunks | chunk addressing (see 12. Chunked Resources) |
Encryption itself is out of scope of the server entirely: the client holds all
keys and performs all encryption, decryption, and index blinding, as required by
G.3 Stored data is opaque to the storage provider. The server enforces WAS
authorization, maximum-privacy 404s, and policies over the ciphertext
unchanged; the encryption is defense in depth layered on top, not a replacement
for the WAS authorization model.
A Collection realizing this profile declares the encryption descriptor
{ "scheme": "edv", "version": 1 } in its Collection Description (see
10.1 Collection Data Model; a bare { "scheme": "edv" } is equivalent, since
an absent version means 1). The wire version is the integer registry key
of the envelope format, distinct from this appendix's own "v0.1" maturity
label. The edv scheme's envelope wire format and the
server's structural fail-closed validation of it are defined by the
C. Encryption Scheme Registry and are not restated here. A server that
recognizes the edv scheme thereby guarantees, without holding any key, that a
plaintext write into the Collection is rejected -- so the profile inherits the
registry's fail-closed property for free.
Key management -- how the JWE's recipient keys are chosen, distributed, rotated,
or organized into epochs for multi-recipient sharing -- is deliberately outside
the scope of both this profile and this specification, exactly as it is for a
native EDV: those keys live in the client, and the server sees only opaque
envelopes. Multi-recipient encryption is carried either directly, in the JWE
recipients structure the C. Encryption Scheme Registry describes, or
through the key-epoch indirection of C.4 Key Epochs, whose public
bookkeeping (the descriptor's epochs roster and the Resource epoch stamp)
the server stores and serves without interpreting; the client-side epoch
construction is specified in [WAS-EC].
A client stores each EDV document as the EDV Encrypted Document envelope -- the
JSON object { id, sequence, indexed?, jwe } -- as the Resource's content, at
the Resource id equal to the EDV document id. The envelope is the edv scheme's
registered wire format (see C. Encryption Scheme Registry); a client SHOULD
write it under the JWE JSON Serialization media type application/jose+json
([RFC7516]) where the server registers a parser for it, and MAY fall back to
application/json, which an unmodified WAS server accepts. The plaintext type of
the resource, and any user-visible metadata, ride inside the JWE and are never
server-visible; the server stores one opaque envelope regardless of what the
decrypted document is.
EDV gives every document a monotonic sequence and enforces previous + 1
atomically server-side. This profile maps that onto WAS conditional writes (see
7.2 Conditional Requests):
PUT carrying If-None-Match: *, so a collision with an
existing document surfaces as precondition-failed (412) rather than a
silent overwrite.sequence to previous + 1,
and writes it back with If-Match pinned to the ETag observed on that read,
so a concurrent writer's stale update is a 412 rather than a lost update.A client SHOULD use these preconditions only against a backend advertising
conditional-writes (see 14.1 Backend Data Model). Against a backend without
it, the mapping degrades to advisory: the sequence is still carried in the
envelope but is not enforced, and writes are last-writer-wins -- the floor the
profile reaches on any conformant WAS server.
A large or streamed EDV document is stored as chunks, using WAS chunk addressing
(see 12. Chunked Resources) against a backend advertising chunked-streams:
stream metadata --
{ sequence, chunks }, where chunks is the total chunk count -- so a reader
can learn the extent of the stream from the document alone.i in 0 .. chunks - 1, the client serializes the EDV chunk
object { index, offset, sequence, jwe } to JSON and PUTs it to chunk index
i under the application/octet-stream content type. The octet-stream type is
deliberate: it routes the chunk through the server's raw-binary write path,
which is bounded by the backend's maxUploadBytes (tens of MiB) rather than
the much smaller body-size limit servers typically impose on JSON-parsed
requests (the reference server's is 1 MiB), which a full encrypted chunk
would exceed. The server stores the bytes verbatim and never parses them, so
a reader decodes and parses the chunk object back client-side.stream.chunks, fetches chunk
indexes 0 .. chunks - 1 (see 12.3 Read Chunk Operation), and decrypts each
jwe client-side to reassemble the stream.jwe is an AEAD ciphertext) but
does **not** authenticate a chunk's *position* in the stream or the stream's
*total length*: the index, offset, and count are carried in plaintext framing and
in the (individually authenticated but not cross-linked) chunk objects. A
malicious server that reorders, drops, duplicates, or truncates chunks is
therefore **not** reliably detectable by the client with this framing. A hardened
framing -- a per-stream authenticated header, an index-bound AAD on each chunk,
and an authenticated total length, in the manner of a Cryptomator-style file
header -- closes this gap and is planned upstream in the EDV specification. When
it lands, this profile will adopt it under a new scheme version; until
then, a deployment that does not trust its storage provider for availability and
ordering integrity SHOULD treat streamed documents accordingly.
Content search over encrypted documents uses the blinded-index query profile
(see D.2 blinded-index profile): a client attaches blinded (HMAC'd)
indexed attributes to the envelope and queries them via
POST /space/{space_id}/{collection_id}/query. Blinded-attribute uniqueness
(unique: true) is enforced server-side by that profile (see
D.2 blinded-index profile, Unique blinded attributes). Both require a
backend advertising the blinded-index-query feature; a client SHOULD gate their
use on it.
Relative to a dedicated EDV server, this profile reaches full parity only for the features whose backend affordances the target backend advertises, and degrades to a client-side floor otherwise:
conditional-writes. Without it, EDV's
previous + 1 guarantee is advisory (last-writer-wins), as described in
E.4 Sequence mapping.unique: true enforcement require
blinded-index-query. Without it, a client cannot query blinded attributes at
the server or rely on it to enforce uniqueness; it must fetch-and-filter
client-side (or, as the reference codec does, mint restrict-mode document ids
and keep all searchable metadata inside the envelope), and a uniqueness
constraint is at best a racy client-side read-then-write.For a small single-writer Collection (for example, a credential wallet) the floor alone is fully usable; a large or multi-writer Collection wants a backend that advertises the affordances above.
This appendix is normative.
This specification uses [RFC9457] Problem Details for HTTP APIs for error
responses (see 6. Error Handling). The type property of a problem
response is a URI identifying the kind of problem. Following [RFC9457],
a type is reused across operations: a single kind such as
invalid-id is emitted by Create, Update, and Read operations
across Spaces, Collections, and Resources alike. The per-occurrence specifics
belong in the errors array (detail and an optional pointer), and the
short human-readable summary in title.
Each type URI is a fragment anchor into this registry. The status codes
listed below are typical; a single kind MAY be returned with more than one
status code depending on the operation.
type URI |
Anchor | Typical status | Description |
|---|---|---|---|
https://wallet.storage/spec#not-found |
not-found | 404 | The resource (Space, Collection, or Resource) does not exist, or the caller is not authorized to access it. These two conditions are deliberately indistinguishable -- see the privacy note below. |
https://wallet.storage/spec#invalid-id |
invalid-id | 400 | A Space, Collection, or Resource id is missing or not URL-safe. |
https://wallet.storage/spec#reserved-id |
reserved-id | 409 | A client-supplied id collides with a B. Reserved Path Segment Registry segment. |
https://wallet.storage/spec#id-conflict |
id-conflict | 409 | A client-supplied id in a POST create operation already exists. (Create-or-replace by id is done idempotently via PUT, which does not conflict.) |
https://wallet.storage/spec#invalid-request-body |
invalid-request-body | 400 | The request body is missing or invalid (e.g. a required property is absent). Entries in errors SHOULD carry a pointer to the offending field. |
https://wallet.storage/spec#invalid-cursor |
invalid-cursor | 400 | A pagination cursor query parameter is malformed or can no longer be honored (e.g. an expired snapshot). See A. Pagination. |
https://wallet.storage/spec#missing-content-type |
missing-content-type | 400 | A required Content-Type header is missing. |
https://wallet.storage/spec#missing-authorization |
missing-authorization | 401 | Required Authorization / Capability-Invocation headers (or proof of possession) are missing. |
https://wallet.storage/spec#invalid-authorization-header |
invalid-authorization-header | 400 | An Authorization, Capability-Invocation, or Digest header is malformed, unparseable, or failed verification. |
https://wallet.storage/spec#controller-mismatch |
controller-mismatch | 400 | The capability invocation in a Create Space request is not currently authorized by the controller supplied in the request body: it is neither signed by that DID nor accompanied by a valid, unexpired delegation chain rooted in it. Servers SHOULD differentiate the cause (chain rooted elsewhere, expired delegation, failed proof) in the detail string where they can; see 8.1.2 Create Space Errors. |
https://wallet.storage/spec#unsupported-backend |
unsupported-backend | 409 | A requested backend id is not in the space's 14.2 Space Backends Available list. |
https://wallet.storage/spec#encryption-immutable |
encryption-immutable | 409 | A Collection update tried to change the scheme, decrease or remove the version, or clear an existing encryption descriptor. The descriptor is set-once, version-monotonic: declaring it on a Collection that lacks one is allowed (and re-declaring the standing values is a no-op), but changing its scheme, moving its version backward, or clearing it on a populated Collection would corrupt the stored, client-encrypted Resources. See 10.1 Collection Data Model. |
https://wallet.storage/spec#encryption-scheme-mismatch |
encryption-scheme-mismatch | 422 | A write into an encrypted Collection -- a Resource's content, or the custom object of a Resource's or the Collection's own Metadata -- had a body (or Content-Type) that does not conform to the Collection's declared encryption scheme envelope profile. Reachable only by a caller already authorized to write -- see C. Encryption Scheme Registry. |
https://wallet.storage/spec#unsupported-encryption-scheme |
unsupported-encryption-scheme | 400 | A Collection create/update declared an encryption scheme (or a version of one) the server does not recognize or support. See C. Encryption Scheme Registry. |
https://wallet.storage/spec#precondition-failed |
precondition-failed | 412 | A conditional write's If-Match / If-None-Match precondition evaluated false: the Resource's current version did not match, or a create-if-absent target already exists. Header-driven and distinct from the 409 conflict kinds. See 7.2 Conditional Requests. |
https://wallet.storage/spec#quota-exceeded |
quota-exceeded | 507 | A write was rejected because the target backend's storage quota is exhausted. See 15. Quotas. |
https://wallet.storage/spec#payload-too-large |
payload-too-large | 413 | An upload exceeds the target backend's maxUploadBytes constraint (see 15. Quotas). Note that unlike quota-exceeded, this rejection is per-request: smaller uploads may still succeed. |
https://wallet.storage/spec#unsupported-operation |
unsupported-operation | 501 | An optional operation that this server or the target backend does not support (for example, a per-collection quota report on a backend without per-collection accounting). |
https://wallet.storage/spec#invalid-import |
invalid-import | 400 | An uploaded archive is not a valid WAS space export. The import operation itself is reserved and not yet specified (see B. Reserved Path Segment Registry); this kind is registered ahead of it so implementations converge on one error shape. |
https://wallet.storage/spec#storage-error |
storage-error | 500 | An underlying storage operation failed. |
https://wallet.storage/spec#internal-error |
internal-error | 500 | An unexpected server-side fault with no more specific kind. |
Privacy: the not-found kind is intentionally merged. Under the principle
of maximum privacy (see 6. Error Handling), an unauthorized client MUST NOT
be able to discover the existence of a Space, Collection, or Resource from an
error response. The not-found kind therefore covers both
"resource absent" and "invalid authorization"; implementations MUST NOT split
it into distinguishable type values, and MUST NOT otherwise let the response
(status code, title, or detail) reveal whether the resource exists.
This merging applies to "insufficient-authorization" and "absent-authorization"
against an existing target. It does not apply to request- or
credential-validation failures, which describe the request rather than the
target and so MAY use their own precise types -- see 6. Error Handling.
Privacy: id-conflict is existence-revealing by nature -- a 409 confirms
that the supplied id is taken. For Create Collection and Create Resource,
servers MUST therefore verify the caller's authorization before checking for
a conflict, so that only callers already authorized to create at that level can
observe the signal; an under-authorized caller receives the merged
not-found instead. For Create Space, where any caller permitted to attempt
creation learns the same fact from whether creation succeeds, the disclosure is
inherent -- see the note under 8.1.2 Create Space Errors.
Canonical example responses for the error kinds defined above, referenced by
the per-operation "Errors" lists throughout this specification. The title
strings are illustrative, not normative: for example, the noun in a
not-found title varies with the target (Space, Collection, or Resource),
and a title may name the operation that failed. Kinds not shown here
(missing-content-type, invalid-authorization-header,
invalid-import, storage-error, internal-error) follow the same
shape; they will gain examples as their corresponding sections are drafted.
not-found -- a missing target, or missing or insufficient authorization (deliberately indistinguishable, see the privacy note above):
HTTP/1.1 404 Not Found
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#not-found",
"title": "Resource not found or insufficient authorization."
}
invalid-id -- an id that is not URL-safe (see 4. Identifiers):
HTTP/1.1 400 Bad Request
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#invalid-id",
"title": "Invalid Space id.",
"errors": [
{
"detail": "Space 'id' must be URL-safe.",
"pointer": "#/id"
}
]
}
reserved-id -- a client-supplied id that collides with a segment from the
B. Reserved Path Segment Registry:
HTTP/1.1 409 Conflict
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#reserved-id",
"title": "Invalid collection id (from reserved list)."
}
id-conflict -- a POST create operation supplying an id that already
exists:
HTTP/1.1 409 Conflict
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#id-conflict",
"title": "A Collection with this id already exists.",
"errors": [
{
"detail": "Use PUT to create-or-replace a Collection at a chosen id.",
"pointer": "#/id"
}
]
}
precondition-failed -- a conditional PUT whose If-Match precondition did
not match the Resource's current version (a concurrent write landed first):
HTTP/1.1 412 Precondition Failed
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#precondition-failed",
"title": "The resource was modified by another write.",
"errors": [
{
"detail": "Re-read the current resource, re-apply your change, and retry."
}
]
}
invalid-request-body -- a missing or invalid request body (here, a Create
Space request without the required controller property):
HTTP/1.1 400 Bad Request
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#invalid-request-body",
"title": "Invalid Create Space body.",
"errors": [
{
"detail": "'controller' property is required.",
"pointer": "#/controller"
}
]
}
missing-authorization -- required authorization (here, a proof of possession signature on a Create Space request) is missing:
HTTP/1.1 401 Unauthorized
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#missing-authorization",
"title": "Invalid Create Space request.",
"errors": [
{
"detail": "Valid proof of possession of the 'controller' DID must be provided."
}
]
}
controller-mismatch -- the invocation is not currently authorized by the
DID specified in the controller property of a Create Space request body
(the detail here is the generic catch-all; servers that can tell SHOULD name
the specific cause instead, e.g. "The delegation chain is rooted in a DID
other than the body's 'controller'." or "The delegated capability has
expired."):
HTTP/1.1 400 Bad Request
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#controller-mismatch",
"title": "Invalid Create Space request.",
"errors": [
{
"detail": "The invocation must be authorized by the 'controller' DID in the request body: signed by it, or via a delegation chain rooted in it.",
"pointer": "#/controller"
}
]
}
unsupported-backend -- a backend id that is not part of that space's
14.2 Space Backends Available list:
HTTP/1.1 409 Conflict
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#unsupported-backend",
"title": "Unsupported backend id, check the space's 'backends available' list."
}
encryption-immutable -- a Collection update tried to change the scheme,
decrease or remove the version, or clear an
existing encryption descriptor (it is set-once, version-monotonic; see
10.1 Collection Data Model):
HTTP/1.1 409 Conflict
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#encryption-immutable",
"title": "Collection encryption descriptor is immutable."
}
encryption-scheme-mismatch -- a write into an encrypted Collection whose
body (a Resource's content, or the custom object of a metadata write) is
not an envelope of the Collection's declared scheme (observable only by a
caller already authorized to write; see C. Encryption Scheme Registry):
HTTP/1.1 422 Unprocessable Content
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#encryption-scheme-mismatch",
"title": "Body does not conform to the Collection's encryption scheme."
}
quota-exceeded -- a write rejected because the target backend's storage quota is exhausted (see 15. Quotas):
HTTP/1.1 507 Insufficient Storage
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#quota-exceeded",
"title": "Storage quota exceeded for backend 'default'."
}
payload-too-large -- an upload exceeding the target backend's
maxUploadBytes constraint:
HTTP/1.1 413 Content Too Large
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#payload-too-large",
"title": "Upload exceeds the backend's maximum upload size.",
"errors": [
{
"detail": "Upload size 209715200 exceeds 'maxUploadBytes' of 157286400 for backend 'dropbox'."
}
]
}
unsupported-operation -- an optional operation this server or backend does not support (here, a per-collection quota report):
HTTP/1.1 501 Not Implemented
Content-type: application/problem+json
{
"type": "https://wallet.storage/spec#unsupported-operation",
"title": "Backend 'default' does not support per-collection quota reports."
}
This section is non-normative.
This storage specification is intended to support the following goals and requirements.
Users and apps need to be able to use (provision, set up, and start reading and writing to) storage spaces without being connected to the internet.
Although the local-first offline functionality is necessary, writing data to stable internet-accessible URLs for the purposes of sharing them is one of the primary use cases of this specification.
A user needs to be able to write data (that is intended to be world-readable) to a cloud-accessible URL, and be able to send that URL to intended recipients via any out-of-band mechanism such as email, chat, and so on.
User needs to be able to change or revoke permissions at any point after sharing. Note that changed permissions apply only to operations that come after the change (this spec is not intended to solve the general problem of DRM).
The sharing and permission system needs to be primarily based on authorization capabilities (zCaps). It also needs support storage-side authorization policies (even if only as a way for an authorized client to receive an appropriate zcap)
The sharing mechanism needs to be flexible and granular. For example, a given data resource needs to be: world-readable, or readable by groups or categories, or by only those possessing the required authorization capabilities, or by no one except the author or controller, etc
Advanced sharing conditions are also desirable (such as "this share expires after X amount of time" or "this is a one-time share and will expire after the first successful read request")
Replication reconciles the first two requirements (data reads and writes must be offline-capable, but the data must eventually be able to be shared on the web via traditional URLs)
Replication also provides critical availability and disaster recovery functionality
Replication needs to be multi-primary (to reflect the multi-device and multi-client user environment)
Multi-primary replication requires support for a versioning or conflict resolution mechanism
Data, metadata, and permissions all need to be replicated
Authorship and data provenance (the ability to tell which user or service created or edited a given set of data) must work in this permissioned multi-primary-write environment
Intended to serve as a storage backend for credential wallets, and any other client-side (Single Page Applications), server side, desktop, and mobile apps and services.
Data written to storage spaces using this specification needs to be portable:
Authorized agents need to be able to export or backup all the data written, including all corresponding metadata and permissions
The sharing and storage system needs to be able to support web domain independent identifiers. That is, a user must be able to share data at a given URL, then be able to migrate to a different storage service provider (potentially operating on a different web domain than the previous one), and the shared permissions to that data must not break after service migration
While portability (and the not breaking of URLs) is relatively easy to achieve via redirect mechanisms (such as HTTP 301 and 302 redirect codes), this requires the previous service provider to be alive, available, and cooperative. However, this is true only of public-readable URLs, and the moment permissions are involved, cross-domain redirects become almost impossible to implement. In addition, portability from "dead servers" is also required. That is, if a cloud-based service provider disappears (or is otherwise unavailable), but a user still has a backup/export available, they should be able to set up another storage server (on another web domain or network address), and import/restore the data from backup, without shares and permissions breaking. Agents that the data was previously shared with must still be able to find the data at the new storage server location, and their permissions must still work.
Spec needs to support the storage of data in any format -- binary files and objects, structured documents such as JSON or CBOR, contents of relational database tables, graphs, and anything else, all using the same unified metadata, sharing, and permission mechanisms.
Storage-side schema enforcement is available but not required.
Spec needs to be able to support multiple protocols and APIs, such as HTTP, JSON-RPC, DIDComm, local client APIs, and more.
Where appropriate (such as for unstructured text, structured documents, RDBMSs etc), storage needs to be queryable or searchable
Any query/search mechanism needs to work well with the sharing/permission and replication requirements
All cryptography has a half-life.
Any cryptographic operations (such as hashing, signatures, and encryption) used in this specification must be able to be obsoleted or upgraded, as techniques and algorithms break. To put it another way, the spec cannot "hardcode" any given algorithm (although it can recommend current best practices)
Implementations of this spec need to be usable with FIPS-compliant cryptographic algorithms
In a "zero trust" storage environment, the sync and replication nodes are assumed to be untrusted: they hold only ciphertext, enforce no authorization of their own, and encryption alone serves as the access control mechanism.
This storage specification is intentionally positioned to not be used in such environments. All encryption has an unpredictable half-life, and some use cases do not permit relying on encryption only for access control.
What this specification pursues instead is a combination of the two: encryption protects data at rest and in transit, while minimally trusted servers independently enforce the applicable policy on every request. A broken cipher alone therefore does not grant an attacker access.
This section is non-normative.
This section will be submitted to the Internet Engineering Steering Group (IESG) for review, approval, and registration with IANA.
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in:
Referenced in: