From d85db5d1477f1074a8f7b4b03ca8903a69237b34 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Mon, 27 Jul 2026 17:06:00 +1000 Subject: [PATCH] Document movable collection authority --- .github/workflows/ci.yml | 4 +- .github/workflows/deploy.yml | 4 +- .../schemas/connect-protocol.v1.schema.json | 142 +++++++++++++++++- src/components/DocsLayout.astro | 4 +- src/data/conformance.json | 2 +- src/pages/connect/index.astro | 63 +++++++- src/pages/sdk/api/index.astro | 4 +- src/pages/sdk/index.astro | 8 +- src/pages/sdk/offline-sync/index.astro | 53 +++---- src/pages/sdk/portable-apps/index.astro | 2 +- src/pages/sdk/routing/index.astro | 37 ++--- 11 files changed, 261 insertions(+), 62 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf3df57..22cc108 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,14 +25,14 @@ jobs: uses: actions/checkout@v7 with: repository: mdbase-dev/mdbase-connect - ref: 8e960317f0886f8c504023dad873cf8cec82fd99 + ref: c4fd2f801b12686e1fd1bd25de4a72cf9e14bc5c path: .sources/mdbase-connect - name: Check out Rust implementation uses: actions/checkout@v7 with: repository: callumalpass/mdbase-rs - ref: fafea262cffbce1205563a580e4d5850e7e942b2 + ref: e9297f465f779d0bf6bb3a361abf16eaf80a6944 path: .sources/mdbase-rs - name: Check out TypeScript implementation diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e3a9b5c..3e733be 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,14 +32,14 @@ jobs: uses: actions/checkout@v7 with: repository: mdbase-dev/mdbase-connect - ref: 8e960317f0886f8c504023dad873cf8cec82fd99 + ref: c4fd2f801b12686e1fd1bd25de4a72cf9e14bc5c path: .sources/mdbase-connect - name: Check out Rust implementation uses: actions/checkout@v7 with: repository: callumalpass/mdbase-rs - ref: fafea262cffbce1205563a580e4d5850e7e942b2 + ref: e9297f465f779d0bf6bb3a361abf16eaf80a6944 path: .sources/mdbase-rs - name: Check out TypeScript implementation diff --git a/public/connect/schemas/connect-protocol.v1.schema.json b/public/connect/schemas/connect-protocol.v1.schema.json index 42786c8..14f9fdb 100644 --- a/public/connect/schemas/connect-protocol.v1.schema.json +++ b/public/connect/schemas/connect-protocol.v1.schema.json @@ -6,7 +6,11 @@ "oneOf": [ { "$ref": "#/$defs/relayOperationRequest" }, { "$ref": "#/$defs/relayOperationResponse" }, - { "$ref": "#/$defs/relayPolicySnapshot" } + { "$ref": "#/$defs/relayPolicySnapshot" }, + { "$ref": "#/$defs/authorizationOfferRequest" }, + { "$ref": "#/$defs/authorizationOfferResponse" }, + { "$ref": "#/$defs/authorizationActivationRequest" }, + { "$ref": "#/$defs/authorizationActivationResponse" } ], "$defs": { "uuid": { @@ -14,7 +18,7 @@ "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$" }, "operation": { - "enum": ["describe", "changes", "read", "query", "list_views", "execute_view", "read_view_source", "create_view_source", "update_view_source", "delete_view_source", "validate", "create", "update", "delete", "rename", "read_type", "create_type", "update_type", "list_timers", "put_timer", "cancel_timer", "reconcile_timers"] + "enum": ["describe", "changes", "read", "query", "list_views", "execute_view", "read_view_source", "create_view_source", "update_view_source", "delete_view_source", "validate", "create", "update", "delete", "rename", "read_type", "create_type", "update_type", "list_timers", "put_timer", "cancel_timer", "reconcile_timers", "sync"] }, "contractRequirement": { "type": "object", @@ -145,6 +149,140 @@ "encryption": { "$ref": "#/$defs/grantEncryption" } } }, + "authorizationCollectionOffer": { + "type": "object", + "additionalProperties": false, + "required": ["collection_id", "display_name", "spec_version", "contracts"], + "properties": { + "collection_id": { "$ref": "#/$defs/uuid" }, + "display_name": { "type": "string", "minLength": 1, "maxLength": 200 }, + "spec_version": { "type": "string", "minLength": 1, "maxLength": 30 }, + "contracts": { + "type": "array", + "maxItems": 100, + "items": { "$ref": "#/$defs/contractRequirement" } + } + } + }, + "applicationRequirements": { + "type": "object", + "additionalProperties": false, + "required": ["contracts"], + "properties": { + "contracts": { + "type": "array", + "maxItems": 20, + "items": { "$ref": "#/$defs/contractRequirement" } + }, + "access": { "enum": ["contract", "full_collection"] } + } + }, + "typeProvision": { + "type": "object", + "additionalProperties": false, + "required": ["name", "document", "provides"], + "properties": { + "name": { "type": "string", "minLength": 1, "maxLength": 100 }, + "path": { "type": "string", "minLength": 1, "maxLength": 500 }, + "document": { "type": "string", "minLength": 1, "maxLength": 262144 }, + "provides": { + "type": "array", + "maxItems": 20, + "items": { "$ref": "#/$defs/contractRequirement" } + } + } + }, + "applicationProvisions": { + "type": "object", + "additionalProperties": false, + "required": ["types"], + "properties": { + "types": { + "type": "array", + "maxItems": 100, + "items": { "$ref": "#/$defs/typeProvision" } + } + } + }, + "authorizationOfferRequest": { + "type": "object", + "additionalProperties": false, + "required": ["type", "protocol_version", "request_id", "authorization_id"], + "properties": { + "type": { "const": "authorization_offer_request" }, + "protocol_version": { "const": 1 }, + "request_id": { "$ref": "#/$defs/uuid" }, + "authorization_id": { "$ref": "#/$defs/uuid" } + } + }, + "authorizationOfferResponse": { + "type": "object", + "additionalProperties": false, + "required": ["type", "protocol_version", "request_id", "paused", "collections"], + "properties": { + "type": { "const": "authorization_offer_response" }, + "protocol_version": { "const": 1 }, + "request_id": { "$ref": "#/$defs/uuid" }, + "paused": { "type": "boolean" }, + "collections": { + "type": "array", + "maxItems": 1000, + "items": { "$ref": "#/$defs/authorizationCollectionOffer" } + } + } + }, + "authorizationActivationRequest": { + "type": "object", + "additionalProperties": false, + "required": ["type", "protocol_version", "request_id", "authorization_id", "collection_id", "requirements", "provisions", "grant"], + "properties": { + "type": { "const": "authorization_activation_request" }, + "protocol_version": { "const": 1 }, + "request_id": { "$ref": "#/$defs/uuid" }, + "authorization_id": { "$ref": "#/$defs/uuid" }, + "collection_id": { "$ref": "#/$defs/uuid" }, + "requirements": { "$ref": "#/$defs/applicationRequirements" }, + "provisions": { "$ref": "#/$defs/applicationProvisions" }, + "grant": { "$ref": "#/$defs/grantPolicy" } + } + }, + "authorizationActivationResponse": { + "type": "object", + "additionalProperties": false, + "required": ["type", "protocol_version", "request_id", "ok", "contracts"], + "properties": { + "type": { "const": "authorization_activation_response" }, + "protocol_version": { "const": 1 }, + "request_id": { "$ref": "#/$defs/uuid" }, + "ok": { "type": "boolean" }, + "contracts": { + "type": "array", + "maxItems": 100, + "items": { "$ref": "#/$defs/contractRequirement" } + }, + "error": { + "type": "object", + "additionalProperties": false, + "required": ["code", "message"], + "properties": { + "code": { "type": "string", "minLength": 1 }, + "message": { "type": "string", "minLength": 1 }, + "details": true + } + } + }, + "allOf": [ + { + "if": { "properties": { "ok": { "const": true } }, "required": ["ok"] }, + "then": { + "not": { "properties": { "error": true }, "required": ["error"] } + }, + "else": { + "required": ["error"] + } + } + ] + }, "notificationCriterion": { "type": "object", "additionalProperties": false, diff --git a/src/components/DocsLayout.astro b/src/components/DocsLayout.astro index 78c4687..ff839ef 100644 --- a/src/components/DocsLayout.astro +++ b/src/components/DocsLayout.astro @@ -26,7 +26,7 @@ const docGroups = [ { href: "/sdk/contracts/", label: "Contracts and adapters", key: "contracts" }, { href: "/sdk/authorization/", label: "Authorization", key: "authorization" }, { href: "/sdk/operations/", label: "Records and operations", key: "operations" }, - { href: "/sdk/routing/", label: "Local and hosted routes", key: "routing" }, + { href: "/sdk/routing/", label: "Authority routes", key: "routing" }, { href: "/sdk/testing/", label: "Testing", key: "testing" }, { href: "/sdk/notifications/", label: "Notifications and timers", key: "notifications" }, { href: "/sdk/offline-sync/", label: "Offline sync", key: "offline-sync" } @@ -66,7 +66,7 @@ const docGroups = [
@mdbase/connect - 0.1.0-beta.1 + 0.1.0-beta.7
diff --git a/src/data/conformance.json b/src/data/conformance.json index 82d4009..bbbb1f6 100644 --- a/src/data/conformance.json +++ b/src/data/conformance.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-07-26", + "generated_at": "2026-07-27", "spec_version": "0.3.0", "implementations": [ { diff --git a/src/pages/connect/index.astro b/src/pages/connect/index.astro index c8c018e..56b5cd1 100644 --- a/src/pages/connect/index.astro +++ b/src/pages/connect/index.astro @@ -80,7 +80,8 @@ const steps = [

Local and hosted collections

Applications use the same SDK and operation envelopes. Routing changes - according to where the authoritative collection lives. + according to which connector or provider currently holds the collection's + authority.

@@ -116,6 +117,64 @@ const steps = [ +
+

Why one movable authority?

+

+ Authority is a coordination role, not the only copy of a collection. + Writable mirrors can keep ordinary Markdown on several devices, accept + changes while offline, and synchronize them later. One authority orders + the final accepted changes, enforces collection-wide rules, and gives + every mirror the same history to converge on. +

+

+ Removing that role would not remove coordination. It would require every + peer—including filesystem editors that only know how to rewrite a + Markdown file—to preserve causal history and agree continuously about + concurrent edits, renames, deletes, schema changes, revocation, and + garbage collection. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConcernEqual peers would requireOne movable authority provides
Ordinary Markdown toolsCRDT or event metadata preserved by every writerOpen files remain the editable local format
Concurrent changesA deterministic merge for every body and frontmatter valueRevision checks, durable mutation receipts, and explicit conflicts
Renames, deletes, and configurationDistributed agreement over paths, tombstones, schemas, and constraintsOne canonical order for collection-wide decisions
Offline devicesSplit-brain reconciliation and peer-membership machineryQueued local work that safely converges when the mirror reconnects
+
+ The role can move without changing the collection. +

+ Authority transfer preserves the collection and record identities, + advances an authority epoch, and retires the previous authority. When + the previous authority was a local folder, that folder can continue as + a mirror. A stale authority cannot accept writes after cutover. +

+
+ +

The authorization sequence

@@ -203,7 +262,7 @@ const steps = [
Authority
-
The local connector or hosted provider that controls the main copy and makes the final authorization decision.
+
The movable coordination role that orders accepted changes, enforces collection rules, and makes the final authorization decision.
Control plane
diff --git a/src/pages/sdk/api/index.astro b/src/pages/sdk/api/index.astro index 54b1910..7c382b1 100644 --- a/src/pages/sdk/api/index.astro +++ b/src/pages/sdk/api/index.astro @@ -265,7 +265,7 @@ const error = `try { -

Connection, routing, and hosted sync

+

Connection, routing, and sync

@@ -280,7 +280,7 @@ const error = `try { - + diff --git a/src/pages/sdk/index.astro b/src/pages/sdk/index.astro index b95e3e0..53beba7 100644 --- a/src/pages/sdk/index.astro +++ b/src/pages/sdk/index.astro @@ -77,7 +77,7 @@ export async function listOpenTasks(client: MdbaseCollectionClient) { - + @@ -97,7 +97,7 @@ export async function listOpenTasks(client: MdbaseCollectionClient) { - + @@ -143,7 +143,7 @@ export async function listOpenTasks(client: MdbaseCollectionClient) { - + @@ -155,7 +155,7 @@ export async function listOpenTasks(client: MdbaseCollectionClient) { - + diff --git a/src/pages/sdk/offline-sync/index.astro b/src/pages/sdk/offline-sync/index.astro index 0dde310..741c8a2 100644 --- a/src/pages/sdk/offline-sync/index.astro +++ b/src/pages/sdk/offline-sync/index.astro @@ -8,24 +8,24 @@ const createReplica = `import { type ReplicaData } from "@mdbase/connect-sync"; -const hosted = connection.hostedSync(); -if (!hosted) { - throw new Error("Authorize a hosted collection first."); +const sync = connection.sync(); +if (!sync) { + throw new Error("Authorize the sync operation first."); } const initial: ReplicaData = { - replicaId: hosted.replicaId, + replicaId: sync.replicaId, records: {}, pending: [], conflicts: {} }; const store = new IndexedDbReplicaStore( - \`tasks:\${hosted.collectionId}\`, + \`tasks:\${sync.collectionId}\`, initial ); -const replica = new OfflineReplica(hosted.transport, store); +const replica = new OfflineReplica(sync.transport, store); await replica.initialize();`; const mutate = `const optimistic = await replica.queueCreate({ @@ -55,7 +55,7 @@ const promote = `mdbase-mirror promote ./worklog-mirror`; -

Require a hosted collection

+

Require sync access

- Add "collection_kind": "hosted" to the manifest requirements - when the application needs an always-online authority and{" "} - hostedSync(). The same application can still maintain an - IndexedDB cache and operate while offline. + Request the sync operation, then call{" "} + connection.sync(). The returned transport is the same whether + the collection authority is remote, directly connected, or reached through + Connect's relay. Add "collection_kind": "hosted" only when the + application specifically requires an always-online hosted authority.

Add both packages: @@ -81,9 +82,8 @@ const promote = `mdbase-mirror promote ./worklog-mirror`;

Create a persistent replica

- The SDK keeps the short-lived provider capability private and refreshes it - before sync calls. The sync package receives only the provider-neutral - transport. + The SDK keeps authority credentials and route selection private. The sync + package receives only the provider-neutral transport.

Queue optimistic local mutations

@@ -99,31 +99,32 @@ const promote = `mdbase-mirror promote ./worklog-mirror`;

Keeping the local version rebases it as a new idempotent mutation against - the current hosted revision. Keeping the hosted version discards only that - record's queued mutations. Neither choice silently affects other records. + the current authority revision. Keeping the authority version discards only + that record's queued mutations. Neither choice silently affects other + records.

Filesystem mirrors use the same protocol

- mdbase-mirror materializes a hosted collection as Markdown, - either receive-only or writable. On first connection to an existing - directory it compares the complete remote snapshot before writing or - uploading. Differing paths stop for explicit review. + mdbase-mirror materializes a remotely authoritative collection + as Markdown, either receive-only or writable. On first connection to an + existing directory it compares the complete remote snapshot before writing + or uploading. Differing paths stop for explicit review.

-

Move hosted authority to a complete local mirror

- +

Move remote authority to a complete local mirror

+

Promotion starts from a converged full writable mirror and opens a - short-lived approval in Connect. Approval freezes hosted writes at a final + short-lived approval in Connect. Approval freezes remote writes at a final sequence. The CLI pulls through that sequence, proves the exact record and resource manifest, registers the folder with the local connector, and then advances the collection authority epoch.

- Old hosted grants, provider capabilities, and replicas are revoked at + Old remote grants, authority capabilities, and replicas are revoked at cutover. Applications authorize the new local authority explicitly. - Cancellation or expiry before completion restores hosted writes. The + Cancellation or expiry before completion restores remote writes. The command is resumable after local materialization.

diff --git a/src/pages/sdk/portable-apps/index.astro b/src/pages/sdk/portable-apps/index.astro index 755c1a6..a80acca 100644 --- a/src/pages/sdk/portable-apps/index.astro +++ b/src/pages/sdk/portable-apps/index.astro @@ -15,7 +15,7 @@ const manifest = `{ }`; const browserBundle = ``; diff --git a/src/pages/sdk/routing/index.astro b/src/pages/sdk/routing/index.astro index d86af2a..f9962d3 100644 --- a/src/pages/sdk/routing/index.astro +++ b/src/pages/sdk/routing/index.astro @@ -11,7 +11,7 @@ if (status === "permission_required") { } connection.onConnectionChange((info) => { - // "direct", "relay", or "hosted" + // "direct", "relay", or "remote" renderRoute(info?.route); });`; @@ -25,14 +25,14 @@ const options = `const mdbase = new MdbaseConnect({ ---

Routing state belongs to a collection-bound MdbaseConnection. - Different saved collections can be hosted, direct, or relayed at the same + Different saved collections can be remote, direct, or relayed at the same time.

@@ -82,26 +82,27 @@ const options = `const mdbase = new MdbaseConnect({ that intentionally lack end-to-end relay encryption.

-

Hosted-provider route

+

Remote-authority route

- Authorization for a hosted collection returns a short-lived, - grant-bound provider capability. The SDK sends operations directly to the - provider, binds browser requests to the approved callback origin, and - refreshes that capability as needed. + Authorization may return a short-lived, grant-bound remote-authority + capability. The SDK sends operations directly to that authority, binds + browser requests to the approved callback origin, and refreshes the + capability as needed.

- Hosted operations go directly from the application to the provider. The - provider decrypts records to execute canonical mdbase operations and acts as - a trusted data processor. + An mdbase hosted provider is one remote-authority deployment. Its operations + go directly from the application to the provider. The provider decrypts + records to execute canonical mdbase operations and acts as a trusted data + processor.

Portable files use the same route-neutral connection

- A downloaded file can use direct, encrypted-relay, or hosted-provider access - after its short-code approval. For hosted access, its short-lived capability - is bound to the exact grant, collection, scope, operations, expiry, and - opaque Origin: null, and remains in process memory. The - consuming operation API is identical across all three routes. + A downloaded file can use direct, encrypted-relay, or remote-authority + access after its short-code approval. For remote access, its short-lived + capability is bound to the exact grant, collection, scope, operations, + expiry, and opaque Origin: null, and remains in process + memory. The consuming operation API is identical across all three routes.

Native shells

MethodPurpose
checkDirectAccess()Read same-computer availability silently
requestDirectAccess()Prompt from a user gesture where required
disableDirectAccess()Persist a relay-only preference for this app
hostedSync()Return a refresh-aware sync transport for a hosted grant, or null
sync()Return one credential-hiding sync transport for a remote, direct, or relayed authority, or null
pendingMutation()Inspect an interrupted encrypted mutation
resumePendingMutation()Recover its exact durable connector receipt
MdbaseConnectionOne collection's operations, renewal, routing, notifications, and hosted syncOne collection's operations, renewal, routing, notifications, and provider-neutral sync Browser APIs or adapters supplied by a native shell
OfflineReplicaOffline-first applications backed by a hosted collectionOffline-first applications backed by any sync-capable authority @mdbase/connect-sync
@mdbase/connectAuthorization, routes, operations, notifications, and hosted sync handoffAuthorization, routes, operations, notifications, and authority-neutral sync handoff
@mdbase/connect-dev
@mdbase/connect-syncHosted replication state machine, IndexedDB replica, conflicts, and offline mutationsAuthority-neutral replication state machine, IndexedDB replica, conflicts, and offline mutations
@mdbase/connect-webhooks