mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-05 07:48:18 +00:00
Reject unsupported WebApp identity and align family guidance
Reject explicit NFC settings before WebApp services start and when settings are loaded or saved. Preserve Legacy interoperability, and clarify host support and the Commonlib-first publication prerequisite.
This commit is contained in:
1 parent
6f8df8f731
commit
9920da8f94
9 files changed
+272
-38
No files matched your search
@@ -126,6 +126,16 @@ Shared synchronisation code is compiled and typed by the `@vrtmrz/livesync-commo
|
|||||||
|
|
||||||
Changes spanning both repositories must first produce a packed Commonlib artefact which passes its standalone package checks. Install that exact artefact in LiveSync, then run the LiveSync type checks, unit tests, application builds, CLI E2E, and any focused real-Obsidian E2E required by the changed boundary. Replace the temporary artefact reference with the reviewed immutable package version before release.
|
Changes spanning both repositories must first produce a packed Commonlib artefact which passes its standalone package checks. Install that exact artefact in LiveSync, then run the LiveSync type checks, unit tests, application builds, CLI E2E, and any focused real-Obsidian E2E required by the changed boundary. Replace the temporary artefact reference with the reviewed immutable package version before release.
|
||||||
|
|
||||||
|
When a change requires a new Commonlib entry point or behaviour, publish and
|
||||||
|
validate that immutable Commonlib version before releasing a dependent
|
||||||
|
application. Follow the [Commonlib release sequence](https://github.com/vrtmrz/livesync-commonlib/blob/main/docs/releasing.md#coordinating-package-and-application-releases),
|
||||||
|
then update this repository's exact `package.json` dependency and lockfile
|
||||||
|
integrity from the registry. The plug-in, CLI, WebApp, and WebPeer use that root
|
||||||
|
dependency; validate their affected builds and runtime boundaries against the
|
||||||
|
same artefact. Bridge has its own import map and lockfile and must be pinned
|
||||||
|
separately. Local tarball overlays are preparation evidence, not clean registry
|
||||||
|
installation evidence.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
The [Project glossary](docs/glossary.md#developer-and-design-terms) defines the
|
The [Project glossary](docs/glossary.md#developer-and-design-terms) defines the
|
||||||
|
|||||||
@@ -34,10 +34,15 @@ admission checks and supported transports.
|
|||||||
|
|
||||||
The central replication gate, mismatch dialogues, and RedFlag Fetch preparation
|
The central replication gate, mismatch dialogues, and RedFlag Fetch preparation
|
||||||
consume the same effective differences. The P2P transport retains its separate
|
consume the same effective differences. The P2P transport retains its separate
|
||||||
policy: ordinary representation differences warn without rejecting transfer,
|
policy: ordinary representation and case differences warn without rejecting
|
||||||
while its existing passphrase and peer checks still apply. NFC path identity
|
transfer, while its existing passphrase and peer checks still apply. Path
|
||||||
is outside that warning-only policy: P2P refuses version `1`. Sharing assessment
|
identity has a separate required check: both peers must use the same version,
|
||||||
does not make the central replication policy appropriate for every transport.
|
and a version mismatch rejects before replication. Commonlib `0.3.0` includes
|
||||||
|
NFC protocol support for Journal and P2P, but the matching CLI and Obsidian host
|
||||||
|
adoption remains an unreleased extension tracked by
|
||||||
|
[the Unicode path identity design](unicode_path_identity.md). Sharing
|
||||||
|
assessment does not make the central replication policy appropriate for every
|
||||||
|
transport.
|
||||||
|
|
||||||
## Host responsibilities
|
## Host responsibilities
|
||||||
|
|
||||||
@@ -96,7 +101,8 @@ failed transfer into a completed transfer.
|
|||||||
## Verification boundaries
|
## Verification boundaries
|
||||||
|
|
||||||
Commonlib tests protect missing-value interpretation, representation differences,
|
Commonlib tests protect missing-value interpretation, representation differences,
|
||||||
directional consequences, immutable results, central admission, and P2P policy.
|
directional consequences, immutable results, central admission, and P2P policy,
|
||||||
|
including the distinction between ordinary warnings and a path-version mismatch.
|
||||||
LiveSync unit tests protect ordinary versus reconstruction choices, trial-setting
|
LiveSync unit tests protect ordinary versus reconstruction choices, trial-setting
|
||||||
selection, partial-setting adoption, and invalidated decisions. RedFlag tests
|
selection, partial-setting adoption, and invalidated decisions. RedFlag tests
|
||||||
protect the distinction between adopting settings for Fetch and retaining local
|
protect the distinction between adopting settings for Fetch and retaining local
|
||||||
|
|||||||
@@ -22,20 +22,23 @@ and is included in current Commonlib `0.2.2`, used by Self-hosted LiveSync
|
|||||||
`1.0.42`. This later handling is distinct from the initial NFC identity
|
`1.0.42`. This later handling is distinct from the initial NFC identity
|
||||||
release.
|
release.
|
||||||
|
|
||||||
An unreleased extension adds Journal and P2P for the same maintained CLI and
|
Commonlib `0.3.0` publishes the Journal and P2P path-identity protocol
|
||||||
Obsidian hosts, moves the setting to General Settings, and defaults new
|
extension. The matching Self-hosted LiveSync CLI and Obsidian host adoption,
|
||||||
synchronisation environments to NFC. The original dates
|
including moving the setting to General Settings and defaulting new
|
||||||
and reviewed versions above remain the historical baseline.
|
synchronisation environments to NFC, remains an unreleased candidate tracked
|
||||||
|
by [pull request #1309](https://github.com/vrtmrz/obsidian-livesync/pull/1309).
|
||||||
|
The original dates and reviewed versions above remain the historical baseline.
|
||||||
|
|
||||||
## Unreleased shared storage boundary
|
## Unreleased shared storage boundary
|
||||||
|
|
||||||
The CLI adopts Commonlib's shared Unicode resolver and typed physical-I/O
|
The current LiveSync candidate adopts Commonlib's shared Unicode resolver and
|
||||||
ports. Each NFC scan retains one comparison-policy snapshot; directory and
|
typed physical-I/O ports in the CLI, and shared namespace and comparison
|
||||||
|
helpers in Obsidian while retaining its Vault APIs and existing alias-guard
|
||||||
|
policy. Each NFC scan retains one comparison-policy snapshot; directory and
|
||||||
permission errors propagate instead of producing a successful partial scan.
|
permission errors propagate instead of producing a successful partial scan.
|
||||||
Obsidian uses the shared namespace and comparison helpers while retaining its
|
These additions are unreleased and do not change the support status of a
|
||||||
Vault APIs and existing alias-guard policy. The [developer guide](../../devs.md#file-path-handling)
|
published host. The [developer guide](../../devs.md#file-path-handling) links
|
||||||
links the Commonlib design and ADR. This extraction does not change Metadata
|
the Commonlib design and ADR.
|
||||||
IDs, defaults, database migration, or the host support matrix below.
|
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
@@ -43,8 +46,8 @@ IDs, defaults, database migration, or the host support matrix below.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Logical identity | Version `1` uses NFC for Metadata ID and path comparison keys. Existing case handling remains a separate rule. Missing and `0` retain legacy behaviour. |
|
| Logical identity | Version `1` uses NFC for Metadata ID and path comparison keys. Existing case handling remains a separate rule. Missing and `0` retain legacy behaviour. |
|
||||||
| Stored paths and content | Keep the spelling in `Metadata.path` and existing physical Vault names. NFC does not change Chunk IDs or content. |
|
| Stored paths and content | Keep the spelling in `Metadata.path` and existing physical Vault names. NFC does not change Chunk IDs or content. |
|
||||||
| Maintained hosts and transports | CLI and Obsidian with CouchDB; Journal and P2P are added by the unreleased extension. |
|
| Maintained hosts and transports | CLI and Obsidian with CouchDB are released; their Journal and P2P host adoption remains unreleased. See the support matrix below. |
|
||||||
| Other consumers | Direct file access refuses a remote declaring the NFC feature. Other host adapters remain outside the support matrix. |
|
| Other consumers | Direct-file access and other host adapters are covered separately below; shared path primitives alone do not enable NFC in an application. |
|
||||||
| Migration | Rebuild both databases from one complete authoritative Vault, then Fetch each remaining client. Do not rewrite IDs or merge histories in place. |
|
| Migration | Rebuild both databases from one complete authoritative Vault, then Fetch each remaining client. Do not rewrite IDs or merge histories in place. |
|
||||||
|
|
||||||
## Path identity contract
|
## Path identity contract
|
||||||
@@ -228,14 +231,15 @@ updates and deletions.
|
|||||||
|
|
||||||
## Compatibility matrix
|
## Compatibility matrix
|
||||||
|
|
||||||
| Client and remote | Version `0` | Version `1` |
|
| Host or API | Published baseline | Unreleased or host-specific status |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| CLI with CouchDB | Supported with legacy identity. | Supported with NFC path resolution. |
|
| CLI and Obsidian with CouchDB | NFC identity is supported from Self-hosted LiveSync `1.0.38` with Commonlib `0.1.42`. | The shared resolver and typed path ports are in the current LiveSync candidate; they are not part of a published Commonlib package yet. |
|
||||||
| Obsidian with CouchDB | Supported with legacy identity. | Supported with NFC path resolution. |
|
| CLI and Obsidian with Journal | Commonlib `0.3.0` publishes the NFC protocol support. | Matching LiveSync host adoption remains an unreleased candidate tracked by [pull request #1309](https://github.com/vrtmrz/obsidian-livesync/pull/1309). Update and migrate every client before using version `1`. |
|
||||||
| CLI or Obsidian with Journal | Existing behaviour. | Added by the unreleased extension; Rebuild remote journals and Fetch other devices. |
|
| CLI and Obsidian with P2P | Commonlib `0.3.0` publishes the NFC protocol support. | Matching LiveSync host adoption remains an unreleased candidate. All peers must use the same path version; P2P warns and continues for ordinary representation or case differences. |
|
||||||
| CLI or Obsidian with P2P | Existing behaviour. | Added by the unreleased extension; update and migrate every peer. |
|
| Bridge | The published baseline uses Legacy version `0`. | The Bridge integration is an unreleased candidate with explicit per-peer opt-in to version `1`; every peer in a group must use the same version. Its ordinary case mismatch check remains enabled unless the Bridge-wide `disableCheckingConfigMismatch: true` override disables that case check. The override does not bypass a path-version mismatch. |
|
||||||
| Direct file access | Existing behaviour. | Refuses a remote requiring `nfc-path-identity-v1`. |
|
| `DirectFileManipulator` | The published Commonlib baseline refuses a remote which declares `nfc-path-identity-v1`. | An unpublished addition accepts explicit `pathNormalizationVersion` values `0` or `1`, defaulting to `0`, and rejects a populated database in the opposite mode. This admission setting does not provide physical-path resolution. |
|
||||||
| Other host adapters, including WebApp | Existing behaviour, subject to their existing support. | Outside the initial support matrix until physical-path handling is verified. |
|
| WebApp | Legacy version `0` only. | WebApp remains Legacy-only and rejects an explicit version `1` at startup. The browser resolver primitive does not enable NFC in WebApp. |
|
||||||
|
| WebPeer | A vaultless P2P utility, not a filesystem host. | No NFC filesystem support is implied by its P2P role. |
|
||||||
| Older client without this feature | Can use a legacy database. | Cannot join a database declaring `nfc-path-identity-v1`. |
|
| Older client without this feature | Can use a legacy database. | Cannot join a database declaring `nfc-path-identity-v1`. |
|
||||||
|
|
||||||
## Journal and P2P extension
|
## Journal and P2P extension
|
||||||
@@ -291,8 +295,11 @@ The implementation acceptance matrix covers:
|
|||||||
version-`0` clients against an NFC database, and enforcement when ordinary
|
version-`0` clients against an NFC database, and enforcement when ordinary
|
||||||
mismatch warnings are disabled;
|
mismatch warnings are disabled;
|
||||||
- URI round trips, checkpoint invalidation across path versions, and
|
- URI round trips, checkpoint invalidation across path versions, and
|
||||||
rejection for Direct file access; Journal and P2P acceptance and identity
|
rejection by the published Direct file access baseline; the unpublished
|
||||||
mismatch checks belong to the unreleased extension;
|
`DirectFileManipulator` addition covers explicit Legacy/NFC admission.
|
||||||
|
Journal and P2P protocol support is published in Commonlib `0.3.0`, while
|
||||||
|
matching CLI and Obsidian host adoption and end-to-end checks remain
|
||||||
|
unreleased;
|
||||||
- refusal for ambiguous file or parent-directory candidates before local or
|
- refusal for ambiguous file or parent-directory candidates before local or
|
||||||
remote reset, preserving both databases on rejection, within the paths
|
remote reset, preserving both databases on rejection, within the paths
|
||||||
returned by each host's supported listing; and
|
returned by each host's supported listing; and
|
||||||
@@ -417,8 +424,8 @@ This design does not:
|
|||||||
- choose among ambiguous NFC-equivalent files or parents, or merge their
|
- choose among ambiguous NFC-equivalent files or parents, or merge their
|
||||||
content;
|
content;
|
||||||
- automatically change existing settings or database identity;
|
- automatically change existing settings or database identity;
|
||||||
- enable version `1` for Direct file access, WebApp, or an
|
- automatically enable version `1` for WebApp or an unverified host adapter;
|
||||||
unverified host adapter; or
|
or
|
||||||
- claim native iOS or macOS filesystem compatibility.
|
- claim native iOS or macOS filesystem compatibility.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ Automated browser coverage uses Chromium. Other browsers, background execution,
|
|||||||
|
|
||||||
## Path identity compatibility
|
## Path identity compatibility
|
||||||
|
|
||||||
WebApp currently supports **Legacy path identity** (`pathNormalizationVersion: 0`) only. **Unicode NFC path identity** (`1`) is not yet supported. Use legacy identity in WebApp's `.livesync/settings.json` and on every client sharing its CouchDB database or P2P group, including new CLI and Obsidian environments which default to NFC.
|
WebApp currently supports **Legacy path identity** (`pathNormalizationVersion: 0`) only. **Unicode NFC path identity** (`1`) is not yet supported. WebApp rejects NFC settings before creating its services or opening its local database; it reports the reason and leaves the Vault files and settings unchanged. Use legacy identity in WebApp's `.livesync/settings.json` and on every client sharing its CouchDB database or P2P group, including new CLI and Obsidian environments which default to NFC.
|
||||||
|
|
||||||
|
The File System Access API can supply the directory listings and handles needed
|
||||||
|
for NFC resolution. Commonlib provides that adapter, but WebApp has not yet
|
||||||
|
integrated it with its file cache, inventory, and watcher behaviour. API-level
|
||||||
|
support does not enable this application boundary automatically.
|
||||||
|
|
||||||
Changing the setting alone does not migrate an existing NFC database. Migration back to legacy identity requires a coordinated Rebuild and Fetch from a complete, authoritative Vault. Back up the Vault and database, and stop synchronisation on all clients before migrating. Keep WebApp disconnected until the migration is complete. See [Recovery and flag files](../../../docs/recovery.md) before rebuilding.
|
Changing the setting alone does not migrate an existing NFC database. Migration back to legacy identity requires a coordinated Rebuild and Fetch from a complete, authoritative Vault. Back up the Vault and database, and stop synchronisation on all clients before migrating. Keep WebApp disconnected until the migration is complete. See [Recovery and flag files](../../../docs/recovery.md) before rebuilding.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getPathNormalizationVersion } from "@vrtmrz/livesync-commonlib/settings";
|
||||||
import { useP2PSettingsPreparation } from "@/serviceFeatures/useP2PSettingsPreparation";
|
import { useP2PSettingsPreparation } from "@/serviceFeatures/useP2PSettingsPreparation";
|
||||||
/** Browser runtime for Self-hosted LiveSync over the File System Access API. */
|
/** Browser runtime for Self-hosted LiveSync over the File System Access API. */
|
||||||
|
|
||||||
@@ -55,6 +56,14 @@ const DEFAULT_SETTINGS: Partial<ObsidianLiveSyncSettings> = {
|
|||||||
autoSweepPluginsPeriodic: false,
|
autoSweepPluginsPeriodic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function assertWebAppPathIdentity(settings: Partial<ObsidianLiveSyncSettings> | null): void {
|
||||||
|
if (getPathNormalizationVersion(settings ?? {}) === 1) {
|
||||||
|
throw new Error(
|
||||||
|
"WebApp supports Legacy path identity (pathNormalizationVersion: 0) only. Unicode NFC path identity is not supported."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type WebAppRuntimeStatusKind = "info" | "warning" | "error" | "success";
|
export type WebAppRuntimeStatusKind = "info" | "warning" | "error" | "success";
|
||||||
|
|
||||||
export interface WebAppRuntimeOptions {
|
export interface WebAppRuntimeOptions {
|
||||||
@@ -100,6 +109,7 @@ export class WebAppRuntime {
|
|||||||
getSystemVaultName: () => this.rootHandle.name || "livesync-webapp",
|
getSystemVaultName: () => this.rootHandle.name || "livesync-webapp",
|
||||||
settings: {
|
settings: {
|
||||||
save: async (data) => {
|
save: async (data) => {
|
||||||
|
assertWebAppPathIdentity(data);
|
||||||
try {
|
try {
|
||||||
await this.saveSettingsToFile(data);
|
await this.saveSettingsToFile(data);
|
||||||
this.addLog("Saved to .livesync/settings.json", LOG_LEVEL_VERBOSE, "settings");
|
this.addLog("Saved to .livesync/settings.json", LOG_LEVEL_VERBOSE, "settings");
|
||||||
@@ -108,15 +118,17 @@ export class WebAppRuntime {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
load: async () => {
|
load: async () => {
|
||||||
|
let data: Partial<ObsidianLiveSyncSettings> | null = null;
|
||||||
try {
|
try {
|
||||||
const data = await this.loadSettingsFromFile();
|
data = await this.loadSettingsFromFile();
|
||||||
if (data) {
|
|
||||||
this.addLog("Loaded from .livesync/settings.json", LOG_LEVEL_VERBOSE, "settings");
|
|
||||||
return { ...DEFAULT_SETTINGS, ...data } as ObsidianLiveSyncSettings;
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
this.addLog("Failed to load settings; using defaults", LOG_LEVEL_NOTICE, "settings");
|
this.addLog("Failed to load settings; using defaults", LOG_LEVEL_NOTICE, "settings");
|
||||||
}
|
}
|
||||||
|
assertWebAppPathIdentity(data);
|
||||||
|
if (data) {
|
||||||
|
this.addLog("Loaded from .livesync/settings.json", LOG_LEVEL_VERBOSE, "settings");
|
||||||
|
return { ...DEFAULT_SETTINGS, ...data } as ObsidianLiveSyncSettings;
|
||||||
|
}
|
||||||
return DEFAULT_SETTINGS as ObsidianLiveSyncSettings;
|
return DEFAULT_SETTINGS as ObsidianLiveSyncSettings;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -205,6 +217,13 @@ export class WebAppRuntime {
|
|||||||
throw new Error("The WebApp runtime has already been started");
|
throw new Error("The WebApp runtime has already been started");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
assertWebAppPathIdentity(await this.loadSettingsFromFile());
|
||||||
|
} catch (error) {
|
||||||
|
this.showError(`Failed to start: ${String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
// Create service context and hub
|
// Create service context and hub
|
||||||
this.serviceHub = createLiveSyncBrowserServiceHub<ServiceContext>(this.createServiceOptions());
|
this.serviceHub = createLiveSyncBrowserServiceHub<ServiceContext>(this.createServiceOptions());
|
||||||
this.addLog("Self-hosted LiveSync WebApp", LOG_LEVEL_INFO, "initialise");
|
this.addLog("Self-hosted LiveSync WebApp", LOG_LEVEL_INFO, "initialise");
|
||||||
|
|||||||
@@ -167,6 +167,76 @@ describe("WebAppRuntime lifecycle", () => {
|
|||||||
runtimeMocks.updateToDatabase.mockResolvedValue(undefined);
|
runtimeMocks.updateToDatabase.mockResolvedValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function settingsRoot(settings: Partial<ObsidianLiveSyncSettings>): FileSystemDirectoryHandle {
|
||||||
|
return {
|
||||||
|
name: "identity-test-vault",
|
||||||
|
getDirectoryHandle: async () => ({
|
||||||
|
getFileHandle: async () => ({
|
||||||
|
getFile: async () => new File([JSON.stringify(settings)], "settings.json"),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as unknown as FileSystemDirectoryHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
it.each([undefined, 0] as const)("starts with supported legacy identity %s", async (version) => {
|
||||||
|
const runtime = new WebAppRuntime(settingsRoot({ pathNormalizationVersion: version }), {
|
||||||
|
deviceValues: new MemoryValueStore(),
|
||||||
|
});
|
||||||
|
await runtime.start();
|
||||||
|
expect(runtimeMocks.onLoad).toHaveBeenCalledOnce();
|
||||||
|
expect(runtimeMocks.scanDirectory).toHaveBeenCalledOnce();
|
||||||
|
await runtime.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([1, 2] as const)(
|
||||||
|
"rejects unsupported identity %s before constructing services or starting file work",
|
||||||
|
async (version) => {
|
||||||
|
const reportStatus = vi.fn();
|
||||||
|
const runtime = new WebAppRuntime(settingsRoot({ pathNormalizationVersion: version as 1 }), {
|
||||||
|
reportStatus,
|
||||||
|
deviceValues: new MemoryValueStore(),
|
||||||
|
});
|
||||||
|
await expect(runtime.start()).rejects.toThrow(/path identity|path normalisation/i);
|
||||||
|
expect(runtimeMocks.options).toBeUndefined();
|
||||||
|
expect(runtimeMocks.onLoad).not.toHaveBeenCalled();
|
||||||
|
expect(runtimeMocks.onReady).not.toHaveBeenCalled();
|
||||||
|
expect(runtimeMocks.scanDirectory).not.toHaveBeenCalled();
|
||||||
|
expect(runtimeMocks.collectFilesOnStorage).not.toHaveBeenCalled();
|
||||||
|
expect(runtimeMocks.updateToDatabase).not.toHaveBeenCalled();
|
||||||
|
expect(reportStatus).toHaveBeenCalledWith(
|
||||||
|
"error",
|
||||||
|
expect.stringMatching(/path identity|path normalisation/i)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it("rejects NFC during settings reload instead of substituting defaults", async () => {
|
||||||
|
const runtime = new WebAppRuntime(settingsRoot({ pathNormalizationVersion: 1 }), {
|
||||||
|
deviceValues: new MemoryValueStore(),
|
||||||
|
});
|
||||||
|
const options = (
|
||||||
|
runtime as unknown as { createServiceOptions(): LiveSyncBrowserServiceHubOptions<never> }
|
||||||
|
).createServiceOptions();
|
||||||
|
if (!options.settings) throw new Error("The runtime must provide settings callbacks");
|
||||||
|
await expect(options.settings.load()).rejects.toThrow(/WebApp supports Legacy path identity/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects saving unsupported NFC settings before opening the settings directory", async () => {
|
||||||
|
const getDirectoryHandle = vi.fn();
|
||||||
|
const runtime = new WebAppRuntime(
|
||||||
|
{ name: "identity-test-vault", getDirectoryHandle } as unknown as FileSystemDirectoryHandle,
|
||||||
|
{ deviceValues: new MemoryValueStore() }
|
||||||
|
);
|
||||||
|
const options = (
|
||||||
|
runtime as unknown as { createServiceOptions(): LiveSyncBrowserServiceHubOptions<never> }
|
||||||
|
).createServiceOptions();
|
||||||
|
if (!options.settings) throw new Error("The runtime must provide settings callbacks");
|
||||||
|
await expect(
|
||||||
|
options.settings.save({ pathNormalizationVersion: 1 } as ObsidianLiveSyncSettings)
|
||||||
|
).rejects.toThrow(/WebApp supports Legacy path identity/);
|
||||||
|
expect(getDirectoryHandle).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("owns core start, directory scan, status reporting, and shutdown", async () => {
|
it("owns core start, directory scan, status reporting, and shutdown", async () => {
|
||||||
const reportStatus = vi.fn();
|
const reportStatus = vi.fn();
|
||||||
const runtime = new WebAppRuntime(createRootHandle(), { reportStatus, deviceValues: new MemoryValueStore() });
|
const runtime = new WebAppRuntime(createRootHandle(), { reportStatus, deviceValues: new MemoryValueStore() });
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Application unit tests are stored in `test/apps/webapp/` and `test/apps/webpeer/
|
|||||||
|
|
||||||
Each application has its own production-bundle smoke-test directory outside `src`:
|
Each application has its own production-bundle smoke-test directory outside `src`:
|
||||||
|
|
||||||
- `test/browser-apps/webapp/` covers Vault selection, OPFS start-up, and isolation between optional P2P settings and the main remote.
|
- `test/browser-apps/webapp/` covers Vault selection, Legacy OPFS start-up, isolation between optional P2P settings and the main remote, and explicit NFC refusal before database, scan, file-write, or remote activity. The refusal case retains the settings and native filename content.
|
||||||
- `test/browser-apps/webpeer/` covers start-up, settings persistence, and reload.
|
- `test/browser-apps/webpeer/` covers start-up, settings persistence, and reload.
|
||||||
- `test/browser-apps/pages/` covers the final GitHub Pages layout, application subpaths, and relative assets.
|
- `test/browser-apps/pages/` covers the final GitHub Pages layout, application subpaths, and relative assets.
|
||||||
|
|
||||||
|
|||||||
@@ -112,9 +112,7 @@ Deno.test({
|
|||||||
"WebApp must not render a peer-menu action without a menu capability"
|
"WebApp must not render a peer-menu action without a menu capability"
|
||||||
);
|
);
|
||||||
await runtimePage.locator(".p2p-control input[type='checkbox']").first().check();
|
await runtimePage.locator(".p2p-control input[type='checkbox']").first().check();
|
||||||
await runtimePage
|
await runtimePage.getByPlaceholder("wss://exp-relay.vrtmrz.net, wss://xxxxx").fill("ws://127.0.0.1:4010/");
|
||||||
.getByPlaceholder("wss://exp-relay.vrtmrz.net, wss://xxxxx")
|
|
||||||
.fill("ws://127.0.0.1:4010/");
|
|
||||||
await runtimePage.getByPlaceholder("anything-you-like").fill("browser-e2e-room");
|
await runtimePage.getByPlaceholder("anything-you-like").fill("browser-e2e-room");
|
||||||
await runtimePage.getByPlaceholder("password").fill("browser-e2e-passphrase");
|
await runtimePage.getByPlaceholder("password").fill("browser-e2e-passphrase");
|
||||||
await runtimePage.getByPlaceholder("iphone-16").fill("browser-e2e-webapp");
|
await runtimePage.getByPlaceholder("iphone-16").fill("browser-e2e-webapp");
|
||||||
@@ -159,3 +157,118 @@ Deno.test({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: "WebApp: rejects NFC settings before opening a database, scanning, writing, or synchronising",
|
||||||
|
sanitizeOps: false,
|
||||||
|
sanitizeResources: false,
|
||||||
|
async fn() {
|
||||||
|
const server = await startStaticServer(webAppDist);
|
||||||
|
const browser = await chromium.launch({ headless: true });
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
const assertNoFailures = observePageFailures(page);
|
||||||
|
await page.addInitScript(`
|
||||||
|
Object.defineProperty(globalThis, "showDirectoryPicker", {
|
||||||
|
configurable: true,
|
||||||
|
value: async () => {
|
||||||
|
const originRoot = await navigator.storage.getDirectory();
|
||||||
|
const root = await originRoot.getDirectoryHandle("livesync-webapp-nfc-refusal", { create: true });
|
||||||
|
const settingsDirectory = await root.getDirectoryHandle(".livesync", { create: true });
|
||||||
|
const settingsText = JSON.stringify({
|
||||||
|
pathNormalizationVersion: 1, isConfigured: true, liveSync: true,
|
||||||
|
couchDB_URI: "https://couchdb.example.invalid", couchDB_DBNAME: "nfc-refusal-test"
|
||||||
|
});
|
||||||
|
const settingsWriter = await (await settingsDirectory.getFileHandle("settings.json", { create: true })).createWritable();
|
||||||
|
await settingsWriter.write(settingsText);
|
||||||
|
await settingsWriter.close();
|
||||||
|
const sentinelWriter = await (await root.getFileHandle("Résumé.md".normalize("NFD"), { create: true })).createWritable();
|
||||||
|
await sentinelWriter.write("retained native content");
|
||||||
|
await sentinelWriter.close();
|
||||||
|
const audit = globalThis.webAppIdentityAudit = {
|
||||||
|
settingsText, writes: 0, scans: 0, opens: [], remoteRequests: [],
|
||||||
|
databaseNamesBefore: (await indexedDB.databases()).map(item => item.name).sort()
|
||||||
|
};
|
||||||
|
const open = IDBFactory.prototype.open;
|
||||||
|
IDBFactory.prototype.open = function(...args) {
|
||||||
|
audit.opens.push(String(args[0]));
|
||||||
|
return Reflect.apply(open, this, args);
|
||||||
|
};
|
||||||
|
const createWritable = FileSystemFileHandle.prototype.createWritable;
|
||||||
|
FileSystemFileHandle.prototype.createWritable = function(...args) {
|
||||||
|
audit.writes++;
|
||||||
|
return Reflect.apply(createWritable, this, args);
|
||||||
|
};
|
||||||
|
for (const key of ["entries", "values", Symbol.asyncIterator]) {
|
||||||
|
const original = FileSystemDirectoryHandle.prototype[key];
|
||||||
|
if (typeof original !== "function") continue;
|
||||||
|
FileSystemDirectoryHandle.prototype[key] = function(...args) {
|
||||||
|
audit.scans++;
|
||||||
|
return Reflect.apply(original, this, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const fetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = function(input, ...args) {
|
||||||
|
const url = String(input?.url ?? input);
|
||||||
|
if (url.startsWith("https://couchdb.example.invalid")) {
|
||||||
|
audit.remoteRequests.push(url);
|
||||||
|
return Promise.resolve(new Response("{}", { status: 503 }));
|
||||||
|
}
|
||||||
|
return Reflect.apply(fetch, this, [input, ...args]);
|
||||||
|
};
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
await page.goto(new URL("webapp.html", server.baseUrl).href);
|
||||||
|
await page.locator("#status").filter({ hasText: "Select a vault folder" }).waitFor();
|
||||||
|
await page.getByRole("button", { name: "Choose new vault folder" }).click();
|
||||||
|
await page.locator("#status.error").filter({ hasText: "WebApp supports Legacy path identity" }).waitFor();
|
||||||
|
const state = await page.evaluate(async () => {
|
||||||
|
const globals = globalThis as typeof globalThis & {
|
||||||
|
webAppIdentityAudit: {
|
||||||
|
settingsText: string;
|
||||||
|
writes: number;
|
||||||
|
scans: number;
|
||||||
|
opens: string[];
|
||||||
|
remoteRequests: string[];
|
||||||
|
databaseNamesBefore: Array<string | undefined>;
|
||||||
|
};
|
||||||
|
livesyncApp: { getRuntime(): unknown };
|
||||||
|
};
|
||||||
|
const audit = globals.webAppIdentityAudit;
|
||||||
|
const root = await (
|
||||||
|
await navigator.storage.getDirectory()
|
||||||
|
).getDirectoryHandle("livesync-webapp-nfc-refusal");
|
||||||
|
const settingsDirectory = await root.getDirectoryHandle(".livesync");
|
||||||
|
return {
|
||||||
|
runtime: globals.livesyncApp.getRuntime(),
|
||||||
|
writes: audit.writes,
|
||||||
|
scans: audit.scans,
|
||||||
|
appDatabaseOpens: audit.opens.filter((name) => name !== "livesync-webapp-handles"),
|
||||||
|
remoteRequests: audit.remoteRequests,
|
||||||
|
databaseNamesBefore: audit.databaseNamesBefore,
|
||||||
|
databaseNamesAfter: (await indexedDB.databases()).map((item) => item.name).sort(),
|
||||||
|
settingsUnchanged:
|
||||||
|
(await (await (await settingsDirectory.getFileHandle("settings.json")).getFile()).text()) ===
|
||||||
|
audit.settingsText,
|
||||||
|
sentinel: await (await (await root.getFileHandle("Résumé.md".normalize("NFD"))).getFile()).text(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assertEquals(state.runtime, null);
|
||||||
|
assertEquals(state.writes, 0);
|
||||||
|
assertEquals(state.scans, 0);
|
||||||
|
assertEquals(state.appDatabaseOpens, []);
|
||||||
|
assertEquals(state.remoteRequests, []);
|
||||||
|
assertEquals(state.databaseNamesAfter, state.databaseNamesBefore);
|
||||||
|
assertEquals(state.settingsUnchanged, true);
|
||||||
|
assertEquals(state.sentinel, "retained native content");
|
||||||
|
assertEquals(await page.locator("#vault-selector").isVisible(), true);
|
||||||
|
assertEquals(await page.getByRole("button", { name: "Scan local files", exact: true }).count(), 0);
|
||||||
|
assertNoFailures();
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
await server.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -22,6 +22,10 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
|
|||||||
|
|
||||||
- Pending conflict dialogues and their resolution steps stop using the released logger after the plug-in is disabled. Waiting continuations check for shutdown before logging or starting further resolution work.
|
- Pending conflict dialogues and their resolution steps stop using the released logger after the plug-in is disabled. Waiting continuations check for shutdown before logging or starting further resolution work.
|
||||||
|
|
||||||
|
#### WebApp
|
||||||
|
|
||||||
|
- The experimental WebApp stops before opening its database or processing files when its settings select unsupported NFC path identity. It reports the Legacy-only requirement and preserves the selected Vault files and settings. This does not migrate an NFC database to Legacy identity.
|
||||||
|
|
||||||
#### Translations
|
#### Translations
|
||||||
|
|
||||||
- The notice shown when a translation is available now displays the **Display Language** setting label correctly in English, French, Hebrew, Russian, and Simplified Chinese. (#1298) Thank you to @cwatanab for the contribution!
|
- The notice shown when a translation is available now displays the **Display Language** setting label correctly in English, French, Hebrew, Russian, and Simplified Chinese. (#1298) Thank you to @cwatanab for the contribution!
|
||||||
|
|||||||
Reference in new issue
Block a user