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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ jobs:
release/checksums.txt
release/owd-sync-*.zip
--generate-notes
--title "OWD Sync $GITHUB_REF_NAME (private beta)"
--title "OWD Sync $GITHUB_REF_NAME (alpha)"
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OWD Sync

[![Release](https://img.shields.io/badge/release-0.1.6-90a5ff.svg)](https://github.com/msinclair25/owd-sync/releases/tag/0.1.6)
[![Release](https://img.shields.io/badge/release-0.1.7-90a5ff.svg)](https://github.com/msinclair25/owd-sync/releases/tag/0.1.7)
[![License](https://img.shields.io/badge/license-Apache--2.0-d7ff64.svg)](LICENSE)
[![Private alpha](https://img.shields.io/badge/OWD-private%20alpha-f0a23a.svg)](https://mdevolved.com/#alpha-access)

Expand All @@ -15,7 +15,7 @@ encrypted recovery.

[Request private alpha access](https://mdevolved.com/#alpha-access) ·
[See OWD Platform](https://github.com/msinclair25/owd-platform) ·
[Download OWD Sync 0.1.6](https://github.com/msinclair25/owd-sync/releases/tag/0.1.6)
[Download OWD Sync 0.1.7](https://github.com/msinclair25/owd-sync/releases/tag/0.1.7)

## The connection boundary

Expand All @@ -38,7 +38,7 @@ flowchart LR
| Supports current-library and recovery workflows | Turns synchronization into an unreviewed agent write path |

> [!IMPORTANT]
> OWD Sync `0.1.6` is an alpha release. It is not yet listed in Obsidian
> OWD Sync `0.1.7` is an alpha release. It is not yet listed in Obsidian
> Community Plugins. OWD Platform provides a temporary one-click desktop
> installer for invited testers; BRAT is the fallback. Use synthetic test
> vaults until the Community Plugin publication and personal-vault gates pass.
Expand All @@ -52,7 +52,7 @@ The normal alpha path starts in the authenticated OWD **Vaults** folder:
consent.
2. Fully quit Obsidian with **Obsidian → Quit Obsidian** or **⌘Q**. Closing the
macOS window is not enough.
3. In OWD, choose **Choose vault and install OWD Sync 0.1.6**.
3. In OWD, choose **Choose vault and install OWD Sync 0.1.7**.
4. In current Chrome or Edge, select the exact vault root containing your notes
and hidden `.obsidian` folder—not `.obsidian` itself—and choose **Allow** if
Chrome requests local write access.
Expand All @@ -76,14 +76,14 @@ fallback:
2. Install and enable BRAT from Obsidian Community Plugins, then wait until
BRAT appears in the Command Palette.
3. Open the version-pinned
[OWD Sync BRAT form](obsidian://brat?plugin=msinclair25/owd-sync&version=0.1.6).
[OWD Sync BRAT form](obsidian://brat?plugin=msinclair25/owd-sync&version=0.1.7).
This link opens BRAT's form; it does not finish the installation. Verify the
repository and version, choose **Add Plugin**, and wait for BRAT to finish.
4. Enable OWD Sync `0.1.6` under **Settings → Community plugins**.
4. Enable OWD Sync `0.1.7` under **Settings → Community plugins**.

If the prefilled link does nothing, run **BRAT: Plugins: Add a beta plugin for
testing (with or without version)** from the Command Palette, paste
`https://github.com/msinclair25/owd-sync`, select `0.1.6`, and choose **Add
`https://github.com/msinclair25/owd-sync`, select `0.1.7`, and choose **Add
Plugin**.

Use either the direct installer or BRAT, not both. BRAT is a testing bridge,
Expand All @@ -93,9 +93,9 @@ gates pass.

## Pair one vault

1. Open the exact vault you intend to connect and confirm OWD Sync `0.1.6` is
1. Open the exact vault you intend to connect and confirm OWD Sync `0.1.7` is
enabled.
2. In OWD, choose **I see OWD Sync 0.1.6 — create request**.
2. In OWD, choose **I see OWD Sync 0.1.7 — create request**.
3. Choose **Open Obsidian and pair**.
4. In Obsidian, verify the current vault name and OWD workspace.
5. Choose **Pair and start sync**.
Expand Down Expand Up @@ -134,22 +134,22 @@ plugin from silently expanding an agent's authority.

| Component | Required version |
| ---------------- | ------------------------------------ |
| OWD Sync | `0.1.6` |
| OWD Platform | `1.0.0-alpha.3` |
| OWD Sync | `0.1.7` |
| OWD Platform | `1.0.0-alpha.7` |
| Obsidian desktop | Current alpha-tested desktop release |
| Direct installer | Current Chrome or Edge over HTTPS |

Do not mix `main.js`, `manifest.json`, or `styles.css` from different releases.
The complete versioned package and SHA-256 checksums are available on the
[OWD Sync 0.1.6 release page](https://github.com/msinclair25/owd-sync/releases/tag/0.1.6).
[OWD Sync 0.1.7 release page](https://github.com/msinclair25/owd-sync/releases/tag/0.1.7).
The ZIP is a maintainer diagnostic artifact, not the normal tester installation
path.

## Troubleshooting

### Obsidian reports `unrecognized URI action`

OWD Sync is not loaded at version `0.1.6` in the vault Obsidian opened. Confirm
OWD Sync is not loaded at version `0.1.7` in the vault Obsidian opened. Confirm
the plugin version and enabled state in that exact vault, then reopen the
pairing request. Use OWD's manual fallback if the direct handoff remains
blocked.
Expand Down
13 changes: 6 additions & 7 deletions SOURCE.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# Source provenance

OWD Sync is developed in the private OWD Platform monorepo and promoted to this
sanitized public plugin repository while the complete platform remains in
private beta.
OWD Sync is developed in the public OWD Platform monorepo and promoted to this
focused plugin distribution repository.

The current `0.1.6` public source and assets correspond to:
The current `0.1.7` public source and assets correspond to:

- OWD Platform plugin tag: `owd-sync-v0.1.6`
- source commit: `2322e7077c93549468c983d202ab28bb161142af`
- public release tag: `0.1.6`
- OWD Platform plugin tag: `owd-sync-v0.1.7`
- source commit: `f56dec9cc9d522a5070717283e24a10d5cf70c5d`
- public release tag: `0.1.7`
- plugin ID: `owd-sync`

The public release uses a tag exactly equal to `manifest.json`'s semantic
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "owd-sync",
"name": "OWD Sync",
"version": "0.1.6",
"version": "0.1.7",
"minAppVersion": "1.5.0",
"description": "Private, self-hosted Obsidian sync through your OWD Cloudflare deployment.",
"author": "OWD Platform contributors",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "owd-sync",
"version": "0.1.6",
"version": "0.1.7",
"private": true,
"description": "The explicit vault connection between Obsidian and an owner-controlled OWD workspace.",
"license": "Apache-2.0 AND 0BSD",
Expand All @@ -24,7 +24,7 @@
"dependencies": {
"fast-diff": "1.3.0",
"fflate": "0.8.2",
"js-yaml": "4.3.0",
"js-yaml": "4.3.1",
"partyserver": "0.3.2",
"y-codemirror.next": "0.3.5",
"y-indexeddb": "9.0.12",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions test/socket-ticket-retry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, it } from "vitest";
import { CAPABILITY_REFRESH_INTERVAL_MS } from "../vendor/yaos-src/runtime/capabilityPolicy";
import { SocketTicketHttpError } from "../vendor/yaos-src/sync/socketTicketError";
import {
decideSocketTicketFailure,
SOCKET_TICKET_RETRY_BASE_MS,
SOCKET_TICKET_RETRY_MAX_MS,
socketTicketRetryDelayMs,
} from "../vendor/yaos-src/sync/socketTicketRetry";

describe("socket ticket retry policy", () => {
it.each([401, 403] as const)(
"turns HTTP %s into terminal authorization state with no retry",
(status) => {
const decision = decideSocketTicketFailure(
new SocketTicketHttpError(status),
0,
0.5,
);

expect(decision).toMatchObject({
kind: "fatal",
code: "unauthorized",
status,
});
expect("delayMs" in decision).toBe(false);
if (decision.kind !== "fatal") {
throw new Error("expected a fatal socket-ticket decision");
}
expect(decision.reason).toContain("Re-pair");
},
);

it.each([429, 500, 503])("backs off retryable HTTP %s failures", (status) => {
const decision = decideSocketTicketFailure(
new SocketTicketHttpError(status),
2,
0.5,
);
expect(decision).toEqual({
kind: "retry",
delayMs: SOCKET_TICKET_RETRY_BASE_MS * 4,
nextAttempt: 3,
});
});

it("backs off network failures exponentially with bounded jitter and a hard cap", () => {
expect(socketTicketRetryDelayMs(0, 0)).toBe(24_000);
expect(socketTicketRetryDelayMs(0, 1)).toBe(36_000);
expect(socketTicketRetryDelayMs(1, 0.5)).toBe(60_000);
expect(socketTicketRetryDelayMs(16, 1)).toBe(SOCKET_TICKET_RETRY_MAX_MS);

const decision = decideSocketTicketFailure(new TypeError("offline"), 16, 1);
expect(decision).toEqual({
kind: "retry",
delayMs: SOCKET_TICKET_RETRY_MAX_MS,
nextAttempt: 16,
});
expect(
decideSocketTicketFailure(new TypeError("offline"), Number.NaN, 0.5),
).toEqual({
kind: "retry",
delayMs: SOCKET_TICKET_RETRY_BASE_MS,
nextAttempt: 1,
});
});

it("limits degraded capability polling to once every five minutes", () => {
expect(CAPABILITY_REFRESH_INTERVAL_MS).toBe(5 * 60_000);
});
});
7 changes: 6 additions & 1 deletion vendor/yaos-src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,12 @@ export default class VaultCrdtSyncPlugin extends Plugin {
const waitingForR2 =
!!this.settings.host &&
(!capabilityState || !capabilityState.attachments || !capabilityState.snapshots);
if (waitingForR2 && (this.capabilityUpdateService?.shouldRefreshCapabilities() ?? false)) {
const authRetryBlocked = this.vaultSync?.fatalAuthError ?? false;
if (
waitingForR2 &&
!authRetryBlocked &&
(this.capabilityUpdateService?.shouldRefreshCapabilities() ?? false)
) {
void this.refreshServerCapabilities("background-poll");
}
}, 3000);
Expand Down
3 changes: 3 additions & 0 deletions vendor/yaos-src/runtime/capabilityPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Capability changes require a server deployment, so a five-minute degraded
// poll is responsive without creating a permanent per-client request loop.
export const CAPABILITY_REFRESH_INTERVAL_MS = 5 * 60_000;
4 changes: 3 additions & 1 deletion vendor/yaos-src/runtime/capabilityUpdateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { attachmentSizeCapKB } from "../settings/settingsStore";
import { obsidianRequest } from "../utils/http";
import { formatUnknown } from "../utils/format";
import { compareSemver } from "../utils/semver";
import { CAPABILITY_REFRESH_INTERVAL_MS } from "./capabilityPolicy";

export { CAPABILITY_REFRESH_INTERVAL_MS } from "./capabilityPolicy";

export type PersistedServerCapabilitiesCache = {
host: string;
Expand Down Expand Up @@ -45,7 +48,6 @@ const UPDATE_MANIFEST_URLS = [
"https://github.com/msinclair25/owd-platform/releases/latest/download/update-manifest.json",
] as const;
const UPDATE_MANIFEST_CACHE_MS = 24 * 60 * 60 * 1000;
export const CAPABILITY_REFRESH_INTERVAL_MS = 30_000;

export function isServerCapabilities(value: unknown): value is ServerCapabilities {
if (typeof value !== "object" || value === null) return false;
Expand Down
4 changes: 4 additions & 0 deletions vendor/yaos-src/runtime/connectionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ export class ConnectionController {
this.onlineHandler = () => {
this.deps.log("Network online event — requesting fast reconnect");
this.deps.scheduleTraceStateSnapshot("network-online");
if (this.deps.getVaultSync()?.fatalAuthError) {
this.deps.refreshStatusBar();
return;
}
this.deps.refreshServerCapabilities("network-online");
this.requestFastReconnect("network-online");
};
Expand Down
15 changes: 3 additions & 12 deletions vendor/yaos-src/sync/socketTicket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,14 @@
*/

import { obsidianRequest } from "../utils/http";
import { SocketTicketHttpError } from "./socketTicketError";

export { SocketTicketHttpError } from "./socketTicketError";

// ---------------------------------------------------------------------------
// Typed HTTP error
// ---------------------------------------------------------------------------

/**
* Thrown by fetchSocketTicket when the server responds with a non-200 status.
* Using a typed error instead of parsing the status code out of a string lets
* callers branch on `err.status` rather than regexing an English message.
*/
export class SocketTicketHttpError extends Error {
constructor(readonly status: number) {
super(`socket ticket request failed (${status})`);
this.name = "SocketTicketHttpError";
}
}

/**
* Returns true when the ticket endpoint does not exist on this server —
* clean "old server" signals only:
Expand Down
7 changes: 7 additions & 0 deletions vendor/yaos-src/sync/socketTicketError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** Typed failure returned by the socket-ticket HTTP endpoint. */
export class SocketTicketHttpError extends Error {
constructor(readonly status: number) {
super(`socket ticket request failed (${status})`);
this.name = "SocketTicketHttpError";
}
}
Loading