diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 403340f..cf3df57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v7 with: repository: mdbase-dev/mdbase-connect - ref: e6b8c7d552c74ad6c4163733ae3337302c97e434 + ref: 8e960317f0886f8c504023dad873cf8cec82fd99 path: .sources/mdbase-connect - name: Check out Rust implementation diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5e03015..e3a9b5c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@v7 with: repository: mdbase-dev/mdbase-connect - ref: e6b8c7d552c74ad6c4163733ae3337302c97e434 + ref: 8e960317f0886f8c504023dad873cf8cec82fd99 path: .sources/mdbase-connect - name: Check out Rust implementation diff --git a/site-sources.json b/site-sources.json index 796480c..77da7f2 100644 --- a/site-sources.json +++ b/site-sources.json @@ -5,7 +5,7 @@ }, "connect": { "repository": "mdbase-dev/mdbase-connect", - "ref": "e6b8c7d552c74ad6c4163733ae3337302c97e434" + "ref": "8e960317f0886f8c504023dad873cf8cec82fd99" }, "implementations": [ { diff --git a/src/pages/sdk/api/index.astro b/src/pages/sdk/api/index.astro index 6bb42ef..54b1910 100644 --- a/src/pages/sdk/api/index.astro +++ b/src/pages/sdk/api/index.astro @@ -14,6 +14,12 @@ const constructor = `new MdbaseConnect({ navigate?: (url: string) => void | Promise })`; +const browserLocation = `const collectionLocation = + new MdbaseBrowserLocation(manager, { + collectionParameter: "collection", + fallbackPath: "/" + });`; + const error = `try { const result = unwrapOperation(await connection.update(input)); } catch (error) { @@ -33,6 +39,7 @@ const error = `try { { href: "#constructor", label: "Constructor" }, { href: "#environment", label: "Environment" }, { href: "#authorization", label: "Authorization methods" }, + { href: "#browser-location", label: "Browser location" }, { href: "#operations", label: "Operation methods" }, { href: "#notifications", label: "Notifications and timers" }, { href: "#connection", label: "Connection and sync" }, @@ -165,6 +172,38 @@ const error = `try { discard the unapproved key.

+

MdbaseBrowserLocation

+ +

+ Keep one location helper beside the application's stable + {" "}MdbaseConnect manager. It treats the explicit URL + identity as authoritative and selects a saved fallback only when exactly + one connection exists. +

+ + + + + + + + + + + + + + +
MethodPurpose
selectedCollectionId()Read the explicit bookmark identity, including one that is not currently authorized
activeConnection()Resolve the bookmarked connection or the sole saved fallback
selectConnection(id, options?)Push or replace a cleaned bookmark URL
authorizationReturnTo()Return the current app-local path after removing temporary OAuth parameters
completeAuthorization(callbackUrl?)Complete PKCE and restore a safe same-origin URL for the approved collection
isAuthorizationCallback(url)Recognize browser and native callback URLs with an authorization result
clearAuthorizationCallback(returnTo?)Remove callback parameters after denial or failure
onChange(listener)Observe saved-connection changes plus browser back and forward navigation
+
+ Collection IDs are bookmark-safe locators. +

+ They may appear in history, logs, and shared URLs. They do not authorize + an operation. Connect still requires the application's stored grant and + enforces it at the collection authority. +

+
+

Collection operations

The methods below belong to MdbaseConnection, not the manager. diff --git a/src/pages/sdk/authorization/index.astro b/src/pages/sdk/authorization/index.astro index 485fb35..bef3bb1 100644 --- a/src/pages/sdk/authorization/index.astro +++ b/src/pages/sdk/authorization/index.astro @@ -7,16 +7,17 @@ const capability = connection.authorizationCapabilities([...required]); if (!capability.sufficient) { await connection.requestOperations([...required], { - returnTo: location.pathname + location.search + returnTo: collectionLocation.authorizationReturnTo() }); }`; -const listen = `const stop = connection.onConnectionChange((info) => { - if (!info) { +const listen = `const stop = collectionLocation.onChange(({ connection }) => { + if (!connection) { renderDisconnected(); return; } + const info = connection.info(); renderConnection({ collectionId: info.collectionId, route: info.route, @@ -161,8 +162,9 @@ const { connection } = await connect.authorize({

The route is direct, relay, or {" "}hosted. Display it as status information. The SDK selects - the route. A null connection means the SDK has no usable - authorization. + the route. The browser location subscription also reports back and forward + selection changes. A null connection means the bookmarked + collection has no usable authorization.

Use structured recovery actions

diff --git a/src/pages/sdk/index.astro b/src/pages/sdk/index.astro index 47f51a4..2287a50 100644 --- a/src/pages/sdk/index.astro +++ b/src/pages/sdk/index.astro @@ -63,6 +63,11 @@ export async function listOpenTasks(client: MdbaseCollectionClient) { One collection's operations, renewal, routing, notifications, and hosted sync Browser APIs or adapters supplied by a native shell + + MdbaseBrowserLocation + Bookmarkable collection selection, authorization return URLs, and browser navigation + A stable MdbaseConnect manager and browser history APIs + MdbaseCollectionClient Domain and feature logic that should be independent of OAuth and hosting @@ -84,12 +89,14 @@ export async function listOpenTasks(client: MdbaseCollectionClient) {

Application structure

Create one MdbaseConnect manager at the application boundary. - Resolve the ID in the URL with manager.connection(id), then - pass that MdbaseConnection into feature code while keeping - OAuth and saved-connection state at the boundary. A bound connection never - changes collections underneath a repository, and feature code written - against MdbaseCollectionClient can also run against the - developer sandbox or another conforming transport. + Pair it with one MdbaseBrowserLocation at the browser + application boundary. Resolve its active connection, then pass that + {" "}MdbaseConnection into feature code. The location helper owns + bookmark selection and OAuth return cleanup; the manager owns saved + authorizations. A bound connection never changes collections underneath a + repository, and feature code written against + {" "}MdbaseCollectionClient can also run against the developer + sandbox or another conforming transport.

A downloaded HTML file supplies diff --git a/src/pages/sdk/quickstart/index.astro b/src/pages/sdk/quickstart/index.astro index 1e6ac2b..f6eff16 100644 --- a/src/pages/sdk/quickstart/index.astro +++ b/src/pages/sdk/quickstart/index.astro @@ -19,6 +19,7 @@ const install = `pnpm add @mdbase/connect pnpm add -D @mdbase/connect-dev`; const client = `import { + MdbaseBrowserLocation, MdbaseConnect, type JsonObject } from "@mdbase/connect"; @@ -33,34 +34,31 @@ export const mdbase = new MdbaseConnect({ serverUrl: "https://connect.mdbase.dev", manifest: new URL("/.well-known/mdbase-app.json", location.origin).href, redirectUri: new URL("/auth/mdbase/callback", location.origin).href -});`; +}); -const connectButton = `import { mdbase } from "./connect"; +export const collectionLocation = new MdbaseBrowserLocation(mdbase);`; + +const connectButton = `import { collectionLocation, mdbase } from "./connect"; export async function connectCollection() { await mdbase.authorize({ operations: ["describe", "read", "query", "update"], - returnTo: location.pathname + location.search + collectionId: collectionLocation.selectedCollectionId() ?? undefined, + returnTo: collectionLocation.authorizationReturnTo() }); // authorize() navigates to Connect and does not return. }`; -const callback = `import { mdbase } from "./connect"; +const callback = `import { collectionLocation } from "./connect"; export async function completeMdbaseCallback() { - const { connection, returnTo = "/" } = - await mdbase.completeAuthorization(location.href); - const destination = new URL(returnTo, location.origin); - destination.searchParams.set("collection", connection.collectionId); - history.replaceState({}, "", destination); - return connection; // Permanently bound to this collection. + return collectionLocation.completeAuthorization(); }`; -const query = `import { mdbase } from "./connect"; +const query = `import { collectionLocation } from "./connect"; import { unwrapOperation } from "@mdbase/connect"; -const collectionId = new URL(location.href).searchParams.get("collection"); -const collection = collectionId ? mdbase.connection(collectionId) : null; +const collection = collectionLocation.activeConnection(); if (!collection) throw new Error("Choose a collection."); const page = unwrapOperation(await collection.query({ @@ -159,7 +157,9 @@ const validate = `mdbase-connect-dev validate-manifest \\

Call this once on the exact callback route listed in the manifest. PKCE is mandatory. On success, the client stores the grant-bound authorization and - returns the newly approved collection-bound connection. + returns the newly approved collection-bound connection. The location + helper restores the app-local return location, adds its collection ID, and + removes temporary OAuth parameters.

6. Query and update with a revision

@@ -172,11 +172,13 @@ const validate = `mdbase-connect-dev validate-manifest \\
Use a stable collection ID in app URLs.

- A query parameter such as ?collection=<id> works on - static hosts, survives refreshes, and makes a view bookmarkable. Treat an - explicit ID as authoritative: if it is not saved, show the chooser or - reconnect that ID instead of silently opening another collection. Names - are display text and can change. + MdbaseBrowserLocation writes + {" "}?collection=<id>, follows browser back and + forward navigation through onChange(), and auto-selects only + when exactly one connection is saved. An explicit unavailable ID stays + authoritative, so show the chooser or reconnect that ID. Collection IDs + can appear in browser history and logs. They are opaque locators, not + credentials. Names are display text and can change.