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:
vorotamoroz committed 2026-11-03 08:44:31 +00:00
1 parent 6f8df8f731
commit 9920da8f94
9 files changed
+272 -38

No files matched your search

+10
View File
@@ -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
+11 -5
View File
@@ -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
+30 -23
View File
@@ -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
+6 -1
View File
@@ -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.
+24 -5
View File
@@ -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() });
+1 -1
View File
@@ -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.
+116 -3
View File
@@ -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();
}
},
});
+4
View File
@@ -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!