@lapis-notes/api 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +304 -13
- package/README.md +19 -0
- package/dist/__tests__/fixtures/indexed-mutations.json +6862 -0
- package/dist/__tests__/fixtures/memory-storage.json +309 -0
- package/dist/__tests__/fixtures/native-facade-contract.json +3224 -0
- package/dist/__tests__/fixtures/projection-query.json +195 -0
- package/dist/__tests__/fixtures/projection-values.json +60 -0
- package/dist/__tests__/fixtures/search-preparation.json +1513 -0
- package/dist/__tests__/fixtures/search-retrieval.json +15409 -0
- package/dist/__tests__/fixtures/search-semantic-model.json +3691 -0
- package/dist/__tests__/fixtures/search-semantic-retrieval.json +6603 -0
- package/dist/__tests__/fixtures/search-storage.json +409 -0
- package/dist/__tests__/fixtures/task-projection.json +428 -0
- package/dist/__tests__/indexed-mutation-support.js +262 -0
- package/dist/__tests__/projection-query-support.js +40 -0
- package/dist/__tests__/search-retrieval-support.js +47 -0
- package/dist/app-database-host.d.ts +1 -0
- package/dist/app-shell-plugin-persistence.d.ts +3 -2
- package/dist/app-shell-plugin-persistence.js +37 -3
- package/dist/cache.svelte.d.ts +8 -1
- package/dist/cache.svelte.js +103 -5
- package/dist/command.svelte.js +29 -2
- package/dist/components/editor/editor.css +4 -0
- package/dist/components/editor/editor.svelte +24 -11
- package/dist/components/editor/embedded-editor-surface.js +3 -1
- package/dist/components/editor/inline-title-alignment.d.ts +3 -0
- package/dist/components/editor/inline-title-alignment.js +57 -0
- package/dist/configuration.svelte.d.ts +270 -206
- package/dist/configuration.svelte.js +161 -86
- package/dist/context.svelte.d.ts +23 -3
- package/dist/context.svelte.js +68 -30
- package/dist/diagnostics.d.ts +8 -0
- package/dist/diagnostics.js +6 -0
- package/dist/editor-save-context.d.ts +35 -0
- package/dist/editor-save-context.js +132 -0
- package/dist/editor-view-registry.d.ts +10 -0
- package/dist/editor-view-registry.js +44 -4
- package/dist/editor.svelte.d.ts +19 -1
- package/dist/editor.svelte.js +528 -55
- package/dist/embed-registry.d.ts +10 -0
- package/dist/embed-registry.js +15 -0
- package/dist/file-editing-failures.d.ts +2 -0
- package/dist/file-editing-failures.js +29 -0
- package/dist/file-editing-native-issues.d.ts +19 -0
- package/dist/file-editing-native-issues.js +65 -0
- package/dist/file-editing-sessions.d.ts +76 -0
- package/dist/file-editing-sessions.js +367 -0
- package/dist/file-manager.d.ts +9 -1
- package/dist/file-manager.js +45 -3
- package/dist/file-state-store.d.ts +5 -0
- package/dist/file-state-store.js +34 -0
- package/dist/file-text-conflicts.d.ts +25 -0
- package/dist/file-text-conflicts.js +35 -0
- package/dist/file-text-mutation.d.ts +13 -0
- package/dist/file-text-mutation.js +100 -0
- package/dist/file-text-persistence.d.ts +36 -0
- package/dist/file-text-persistence.js +234 -0
- package/dist/file-text-state-writer.d.ts +34 -0
- package/dist/file-text-state-writer.js +183 -0
- package/dist/files-configuration.d.ts +20 -0
- package/dist/files-configuration.js +47 -0
- package/dist/generated/plugin-host-modules.schema.generated.d.ts +114 -34
- package/dist/generated/plugin-host-modules.schema.generated.js +132 -34
- package/dist/glob.d.ts +3 -0
- package/dist/glob.js +2 -1
- package/dist/icons/changes.d.ts +3 -0
- package/dist/icons/changes.js +16 -0
- package/dist/icons/index.d.ts +5 -1
- package/dist/icons/index.js +44 -3
- package/dist/icons/registry.d.ts +35 -0
- package/dist/icons/registry.js +154 -0
- package/dist/icons/safe-svg.d.ts +2 -0
- package/dist/icons/safe-svg.js +44 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -1
- package/dist/lapis-extension.d.ts +6 -2
- package/dist/lapis-extension.js +2 -0
- package/dist/metadata-store.d.ts +23 -0
- package/dist/metadata-store.js +1 -0
- package/dist/metadata.svelte.js +45 -2
- package/dist/native-background-indexing.d.ts +48 -0
- package/dist/native-background-indexing.js +208 -0
- package/dist/native-database-protocol.d.ts +29 -0
- package/dist/native-database-protocol.js +1 -0
- package/dist/native-editing-intent-lease.d.ts +17 -0
- package/dist/native-editing-intent-lease.js +188 -0
- package/dist/native-editing-intents-protocol.d.ts +70 -0
- package/dist/native-editing-intents-protocol.js +1 -0
- package/dist/native-editing-intents.d.ts +8 -0
- package/dist/native-editing-intents.js +1 -0
- package/dist/native-editing-problems.d.ts +3 -0
- package/dist/native-editing-problems.js +180 -0
- package/dist/native-editing-recovery.d.ts +24 -0
- package/dist/native-editing-recovery.js +198 -0
- package/dist/native-editing-session.d.ts +51 -0
- package/dist/native-editing-session.js +251 -0
- package/dist/native-history-index-protocol.d.ts +80 -0
- package/dist/native-history-index-protocol.js +1 -0
- package/dist/native-history-index.d.ts +6 -0
- package/dist/native-history-index.js +1 -0
- package/dist/native-indexing-protocol.d.ts +63 -0
- package/dist/native-indexing-protocol.js +1 -0
- package/dist/native-participant-protocol.d.ts +67 -0
- package/dist/native-participant-protocol.js +3 -0
- package/dist/native-queue-protocol.d.ts +73 -0
- package/dist/native-queue-protocol.js +3 -0
- package/dist/native-tasks-protocol.d.ts +264 -0
- package/dist/native-tasks-protocol.js +1 -0
- package/dist/native-tasks.d.ts +35 -0
- package/dist/native-tasks.js +79 -0
- package/dist/native-turn-protocol.d.ts +81 -0
- package/dist/native-turn-protocol.js +1 -0
- package/dist/native-turns.d.ts +29 -0
- package/dist/native-turns.js +2 -0
- package/dist/plugin-asset-server.js +3 -2
- package/dist/plugin-dependency-resolver.d.ts +1 -1
- package/dist/plugin-distribution/compatibility.d.ts +1 -1
- package/dist/plugin-distribution/compatibility.js +10 -5
- package/dist/plugin-distribution/installed-plugin-state.js +2 -1
- package/dist/plugin-distribution/installer.d.ts +6 -0
- package/dist/plugin-distribution/installer.js +90 -37
- package/dist/plugin-distribution/manager.d.ts +4 -1
- package/dist/plugin-distribution/manager.js +12 -5
- package/dist/plugin-distribution/registry-cache.js +2 -1
- package/dist/plugin-distribution/registry-source-state.js +2 -1
- package/dist/plugin-distribution/schemas.d.ts +26 -8
- package/dist/plugin-distribution/schemas.js +2 -1
- package/dist/plugin-distribution/types.d.ts +4 -0
- package/dist/plugin-manager.d.ts +21 -5
- package/dist/plugin-manager.js +228 -64
- package/dist/plugin-runtime-entry.d.ts +1 -1
- package/dist/plugin-runtime-entry.js +16 -0
- package/dist/plugin.d.ts +19 -3
- package/dist/plugin.js +110 -44
- package/dist/registry-schema/catalog-v1.d.ts +60 -9
- package/dist/registry-schema/catalog-v1.js +2 -1
- package/dist/registry-schema/catalog-v2.d.ts +60 -9
- package/dist/registry-schema/catalog-v2.js +1 -1
- package/dist/search-result-provider.d.ts +95 -0
- package/dist/search-result-provider.js +42 -0
- package/dist/status-bar.svelte.d.ts +26 -20
- package/dist/status-bar.svelte.js +1 -0
- package/dist/storage/app-database.d.ts +9 -23
- package/dist/storage/app-database.js +41 -34
- package/dist/storage/desktop-native.d.ts +5 -3
- package/dist/storage/desktop-native.js +59 -6
- package/dist/storage/directory-watcher.d.ts +27 -0
- package/dist/storage/directory-watcher.js +201 -0
- package/dist/storage/fs.d.ts +14 -27
- package/dist/storage/fs.js +2 -168
- package/dist/storage/index-projection.js +33 -15
- package/dist/storage/memory-vault-adapter.d.ts +2 -0
- package/dist/storage/memory-vault-adapter.js +14 -1
- package/dist/storage/path.d.ts +6 -2
- package/dist/storage/path.js +8 -2
- package/dist/storage/projection-query-candidates.d.ts +3 -0
- package/dist/storage/projection-query-candidates.js +69 -0
- package/dist/storage/remote-app-database.d.ts +2 -2
- package/dist/storage/remote-app-database.js +1 -1
- package/dist/storage/runtime.d.ts +3 -0
- package/dist/storage/runtime.js +3 -0
- package/dist/storage/search-embedding-record.d.ts +3 -0
- package/dist/storage/search-embedding-record.js +18 -0
- package/dist/storage/turso-app-database.d.ts +2 -0
- package/dist/storage/turso-app-database.js +67 -30
- package/dist/storage/vault-listing.d.ts +3 -0
- package/dist/storage/vault-listing.js +36 -0
- package/dist/storage/vault-reconciliation.d.ts +19 -0
- package/dist/storage/vault-reconciliation.js +137 -0
- package/dist/storage/vault-session.d.ts +8 -0
- package/dist/storage/vault-session.js +10 -2
- package/dist/storage/vault.svelte.d.ts +53 -4
- package/dist/storage/vault.svelte.js +223 -87
- package/dist/sync-editor-flush.d.ts +6 -0
- package/dist/sync-editor-flush.js +22 -0
- package/dist/sync-host-local.d.ts +15 -0
- package/dist/sync-host-local.js +228 -0
- package/dist/sync-host-security.d.ts +24 -9
- package/dist/sync-host-security.js +47 -14
- package/dist/sync-host.d.ts +33 -4
- package/dist/sync-host.js +160 -10
- package/dist/sync-repository.d.ts +106 -0
- package/dist/sync-repository.js +18 -0
- package/dist/vault-change-views.d.ts +4 -0
- package/dist/vault-change-views.js +43 -0
- package/dist/vault-changes.d.ts +37 -0
- package/dist/vault-changes.js +228 -0
- package/dist/view.svelte.d.ts +182 -149
- package/dist/view.svelte.js +39 -21
- package/dist/workspace-close-guard.d.ts +3 -0
- package/dist/workspace-close-guard.js +35 -0
- package/dist/workspace-host-internal.js +10 -2
- package/dist/workspace-layout-normalizer.d.ts +4 -2
- package/dist/workspace-layout-normalizer.js +5 -5
- package/dist/workspace-view-document.d.ts +3 -0
- package/dist/workspace-view-document.js +12 -0
- package/dist/workspace.svelte.d.ts +13 -2
- package/dist/workspace.svelte.js +371 -82
- package/package.json +61 -8
package/dist/sync-host.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TFile } from "./storage/fs";
|
|
2
2
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
3
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
4
|
+
import { ScopedSyncLocalRuntime } from "./sync-host-local";
|
|
4
5
|
import { SyncHostSecurityService, } from "./sync-host-security";
|
|
5
6
|
const ROOT_PREFIX = "sync-host:v1:";
|
|
6
7
|
const DATABASE_GENERATION_KEY = `${ROOT_PREFIX}database-generation`;
|
|
@@ -71,19 +72,39 @@ export class SyncHostService {
|
|
|
71
72
|
database;
|
|
72
73
|
vault;
|
|
73
74
|
clock;
|
|
74
|
-
options;
|
|
75
75
|
#ready;
|
|
76
76
|
#eventQueue = Promise.resolve();
|
|
77
77
|
#runtimeIds = new Map();
|
|
78
78
|
#origins = new Map();
|
|
79
79
|
#security;
|
|
80
|
+
#selectSecurityProvider;
|
|
81
|
+
#securityProviders = new Map();
|
|
82
|
+
#delegatedRuntimes = new Map();
|
|
83
|
+
#localRuntimes = new Map();
|
|
84
|
+
#selectLocalProvider;
|
|
85
|
+
#guardLocalApply;
|
|
86
|
+
#securityCleanup = new Set();
|
|
87
|
+
#stopObserving;
|
|
88
|
+
#disposed = false;
|
|
89
|
+
#disposal;
|
|
90
|
+
options;
|
|
80
91
|
constructor(database, vault, clock = Date.now, options = {}) {
|
|
81
92
|
this.database = database;
|
|
82
93
|
this.vault = vault;
|
|
83
94
|
this.clock = clock;
|
|
84
|
-
|
|
85
|
-
this
|
|
86
|
-
|
|
95
|
+
const { securityStore, securityProvider, localProvider, guardLocalApply, ...retainedOptions } = options;
|
|
96
|
+
this.options =
|
|
97
|
+
securityStore === undefined &&
|
|
98
|
+
securityProvider === undefined &&
|
|
99
|
+
localProvider === undefined &&
|
|
100
|
+
guardLocalApply === undefined
|
|
101
|
+
? options
|
|
102
|
+
: retainedOptions;
|
|
103
|
+
this.#selectSecurityProvider = securityProvider;
|
|
104
|
+
this.#selectLocalProvider = localProvider;
|
|
105
|
+
this.#guardLocalApply = guardLocalApply;
|
|
106
|
+
this.#security = new SyncHostSecurityService(securityStore === undefined ? database : securityStore, (pluginId, runtimeId) => this.#assertRuntime(pluginId, runtimeId), options.security);
|
|
107
|
+
const eventRef = this.vault.on("all", (event, file, context) => {
|
|
87
108
|
if (!(file instanceof TFile) || !this.#isChangeKind(event))
|
|
88
109
|
return;
|
|
89
110
|
const previousPath = typeof context.oldPath === "string" ? context.oldPath : undefined;
|
|
@@ -92,10 +113,12 @@ export class SyncHostService {
|
|
|
92
113
|
: undefined;
|
|
93
114
|
this.#eventQueue = this.#eventQueue
|
|
94
115
|
.then(async () => {
|
|
116
|
+
await this.#ready;
|
|
95
117
|
await this.#recordFileChange(event, file, previousPath, originId);
|
|
96
118
|
})
|
|
97
119
|
.catch(() => undefined);
|
|
98
120
|
});
|
|
121
|
+
this.#stopObserving = () => this.vault.offref(eventRef);
|
|
99
122
|
this.#ready = this.#initialize();
|
|
100
123
|
}
|
|
101
124
|
async ready() {
|
|
@@ -103,6 +126,8 @@ export class SyncHostService {
|
|
|
103
126
|
await this.#eventQueue;
|
|
104
127
|
}
|
|
105
128
|
forPlugin(pluginId, grantedCapabilities) {
|
|
129
|
+
if (this.#disposed)
|
|
130
|
+
throw new Error("Sync host is disposed");
|
|
106
131
|
const normalizedPluginId = requireBoundedString(pluginId, "Plugin ID", 128);
|
|
107
132
|
const runtimeId = randomId();
|
|
108
133
|
const runtimeIds = this.#runtimeIds.get(normalizedPluginId) ?? new Set();
|
|
@@ -116,7 +141,7 @@ export class SyncHostService {
|
|
|
116
141
|
throw new Error(`Plugin ${normalizedPluginId} is not granted ${capability}`);
|
|
117
142
|
}
|
|
118
143
|
};
|
|
119
|
-
|
|
144
|
+
const capabilities = {
|
|
120
145
|
files: {
|
|
121
146
|
getIdentity: async (path) => {
|
|
122
147
|
assertGranted("metadata:identity");
|
|
@@ -304,32 +329,157 @@ export class SyncHostService {
|
|
|
304
329
|
}),
|
|
305
330
|
generatedState: this.#generatedState(normalizedPluginId, runtimeId, () => assertGranted("plugin:generated-state")),
|
|
306
331
|
};
|
|
332
|
+
if (this.#selectLocalProvider) {
|
|
333
|
+
const local = new ScopedSyncLocalRuntime({
|
|
334
|
+
runtimeId,
|
|
335
|
+
assertGranted,
|
|
336
|
+
selectProvider: () => this.#selectLocalProvider(normalizedPluginId),
|
|
337
|
+
guardApply: this.#guardLocalApply,
|
|
338
|
+
});
|
|
339
|
+
this.#localRuntimes.set(runtimeId, local);
|
|
340
|
+
Object.assign(capabilities, { local: local.capabilities });
|
|
341
|
+
}
|
|
342
|
+
let delegated;
|
|
343
|
+
let selected = false;
|
|
344
|
+
for (const group of [
|
|
345
|
+
"crypto",
|
|
346
|
+
"syncIdentity",
|
|
347
|
+
"pairing",
|
|
348
|
+
"membership",
|
|
349
|
+
"recovery",
|
|
350
|
+
]) {
|
|
351
|
+
const local = capabilities[group];
|
|
352
|
+
const guarded = {};
|
|
353
|
+
for (const method of Object.keys(local)) {
|
|
354
|
+
guarded[method] = async (...args) => {
|
|
355
|
+
assertGranted("crypto:nostr-sync");
|
|
356
|
+
if (!selected) {
|
|
357
|
+
const provider = this.#securityProvider(normalizedPluginId);
|
|
358
|
+
if (provider) {
|
|
359
|
+
this.#delegatedRuntimes.set(runtimeId, provider);
|
|
360
|
+
delegated = provider.forRuntime(runtimeId);
|
|
361
|
+
if (!delegated || typeof delegated !== "object")
|
|
362
|
+
throw new Error("Invalid Sync security provider capabilities");
|
|
363
|
+
}
|
|
364
|
+
selected = true;
|
|
365
|
+
}
|
|
366
|
+
assertGranted("crypto:nostr-sync");
|
|
367
|
+
const target = delegated ? delegated[group] : local;
|
|
368
|
+
const operation = target && target[method];
|
|
369
|
+
if (typeof operation !== "function")
|
|
370
|
+
throw new Error(`Invalid Sync security provider operation: ${group}.${method}`);
|
|
371
|
+
const result = await operation.apply(target, args);
|
|
372
|
+
assertGranted("crypto:nostr-sync");
|
|
373
|
+
return result;
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
Object.assign(capabilities, { [group]: guarded });
|
|
377
|
+
}
|
|
378
|
+
return capabilities;
|
|
307
379
|
}
|
|
308
380
|
revokePlugin(pluginId) {
|
|
381
|
+
const runtimes = this.#runtimeIds.get(pluginId);
|
|
309
382
|
this.#runtimeIds.delete(pluginId);
|
|
310
383
|
for (const [token, owner] of this.#origins) {
|
|
311
384
|
if (owner.pluginId === pluginId)
|
|
312
385
|
this.#origins.delete(token);
|
|
313
386
|
}
|
|
314
387
|
this.#security.revokePlugin(pluginId);
|
|
388
|
+
const cleanups = [];
|
|
389
|
+
for (const runtimeId of runtimes ?? []) {
|
|
390
|
+
const provider = this.#delegatedRuntimes.get(runtimeId);
|
|
391
|
+
this.#delegatedRuntimes.delete(runtimeId);
|
|
392
|
+
const local = this.#localRuntimes.get(runtimeId);
|
|
393
|
+
this.#localRuntimes.delete(runtimeId);
|
|
394
|
+
if (!provider && !local)
|
|
395
|
+
continue;
|
|
396
|
+
let cleanup;
|
|
397
|
+
try {
|
|
398
|
+
// Local streams/operations must finish while native crypto custody still exists.
|
|
399
|
+
const localCleanup = local?.revoke();
|
|
400
|
+
cleanup = localCleanup
|
|
401
|
+
? (async () => {
|
|
402
|
+
const result = await Promise.allSettled([localCleanup]);
|
|
403
|
+
await provider?.revokeRuntime(runtimeId);
|
|
404
|
+
if (result[0]?.status === "rejected")
|
|
405
|
+
throw result[0].reason;
|
|
406
|
+
})()
|
|
407
|
+
: Promise.resolve(provider.revokeRuntime(runtimeId));
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
cleanup = Promise.reject(error);
|
|
411
|
+
}
|
|
412
|
+
this.#securityCleanup.add(cleanup);
|
|
413
|
+
cleanups.push(cleanup);
|
|
414
|
+
}
|
|
415
|
+
return this.#drainSecurity(cleanups);
|
|
416
|
+
}
|
|
417
|
+
/** Invalidate first, then wait for every admitted host cleanup and file event. */
|
|
418
|
+
dispose() {
|
|
419
|
+
if (this.#disposal)
|
|
420
|
+
return this.#disposal;
|
|
421
|
+
this.#disposed = true;
|
|
422
|
+
this.#stopObserving();
|
|
423
|
+
const revocations = [...this.#runtimeIds.keys()].map((pluginId) => this.revokePlugin(pluginId));
|
|
424
|
+
this.#disposal = this.#drainSecurity([
|
|
425
|
+
...revocations,
|
|
426
|
+
...this.#securityCleanup,
|
|
427
|
+
this.ready(),
|
|
428
|
+
]);
|
|
429
|
+
return this.#disposal;
|
|
430
|
+
}
|
|
431
|
+
async #drainSecurity(cleanups) {
|
|
432
|
+
const results = await Promise.allSettled(cleanups);
|
|
433
|
+
for (const cleanup of cleanups)
|
|
434
|
+
this.#securityCleanup.delete(cleanup);
|
|
435
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
436
|
+
if (failed?.status === "rejected")
|
|
437
|
+
throw failed.reason;
|
|
438
|
+
}
|
|
439
|
+
#securityProvider(pluginId) {
|
|
440
|
+
if (!this.#securityProviders.has(pluginId)) {
|
|
441
|
+
const provider = this.#selectSecurityProvider?.(pluginId);
|
|
442
|
+
if (provider !== undefined &&
|
|
443
|
+
(!provider ||
|
|
444
|
+
typeof provider.forRuntime !== "function" ||
|
|
445
|
+
typeof provider.revokeRuntime !== "function"))
|
|
446
|
+
throw new Error("Invalid Sync security provider");
|
|
447
|
+
this.#securityProviders.set(pluginId, provider);
|
|
448
|
+
}
|
|
449
|
+
return this.#securityProviders.get(pluginId);
|
|
450
|
+
}
|
|
451
|
+
#assertLocalSecurity(pluginId) {
|
|
452
|
+
requireBoundedString(pluginId, "Plugin ID", 128);
|
|
453
|
+
if (this.#disposed)
|
|
454
|
+
throw new Error("Sync host is disposed");
|
|
455
|
+
if (this.#securityProvider(pluginId))
|
|
456
|
+
throw new Error(`Sync security for ${pluginId} is delegated to its host provider`);
|
|
315
457
|
}
|
|
316
|
-
/**
|
|
317
|
-
|
|
458
|
+
/**
|
|
459
|
+
* Host-controlled enrollment; this operation is intentionally absent from
|
|
460
|
+
* plugin facades.
|
|
461
|
+
*/
|
|
462
|
+
async enrollPluginCrypto(pluginId, passphrase, request) {
|
|
463
|
+
this.#assertLocalSecurity(pluginId);
|
|
318
464
|
return this.#security.enroll(requireBoundedString(pluginId, "Plugin ID", 128), passphrase, request);
|
|
319
465
|
}
|
|
320
466
|
/** Host-controlled unlock; plugins can only observe locked state. */
|
|
321
|
-
unlockPluginCrypto(pluginId, passphrase) {
|
|
467
|
+
async unlockPluginCrypto(pluginId, passphrase) {
|
|
468
|
+
this.#assertLocalSecurity(pluginId);
|
|
322
469
|
return this.#security.unlock(requireBoundedString(pluginId, "Plugin ID", 128), passphrase);
|
|
323
470
|
}
|
|
324
471
|
/** Host-controlled passkey or OS-credential enrollment. */
|
|
325
|
-
enrollPluginCryptoWithWrappingKey(pluginId, wrappingKey, descriptor, request) {
|
|
472
|
+
async enrollPluginCryptoWithWrappingKey(pluginId, wrappingKey, descriptor, request) {
|
|
473
|
+
this.#assertLocalSecurity(pluginId);
|
|
326
474
|
return this.#security.enrollWithWrappingKey(requireBoundedString(pluginId, "Plugin ID", 128), wrappingKey, descriptor, request);
|
|
327
475
|
}
|
|
328
476
|
/** Host-controlled passkey or OS-credential unlock. */
|
|
329
|
-
unlockPluginCryptoWithWrappingKey(pluginId, wrappingKey) {
|
|
477
|
+
async unlockPluginCryptoWithWrappingKey(pluginId, wrappingKey) {
|
|
478
|
+
this.#assertLocalSecurity(pluginId);
|
|
330
479
|
return this.#security.unlockWithWrappingKey(requireBoundedString(pluginId, "Plugin ID", 128), wrappingKey);
|
|
331
480
|
}
|
|
332
481
|
lockPluginCrypto(pluginId) {
|
|
482
|
+
this.#assertLocalSecurity(pluginId);
|
|
333
483
|
this.#security.lock(requireBoundedString(pluginId, "Plugin ID", 128));
|
|
334
484
|
}
|
|
335
485
|
grantPluginNetworkOrigin(pluginId, origin, options) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Durable opaque sync data. Hosts keep this separate from disposable AppDatabase indexes. */
|
|
2
|
+
export interface SealedSyncState {
|
|
3
|
+
version: number;
|
|
4
|
+
epoch: number;
|
|
5
|
+
ciphertext: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
export interface SyncRepository {
|
|
8
|
+
readonly blobs: {
|
|
9
|
+
has(hash: string): Promise<boolean>;
|
|
10
|
+
put(ciphertext: Uint8Array): Promise<{
|
|
11
|
+
hash: string;
|
|
12
|
+
size: number;
|
|
13
|
+
}>;
|
|
14
|
+
get(hash: string): Promise<Uint8Array>;
|
|
15
|
+
};
|
|
16
|
+
readState(): Promise<SealedSyncState | null>;
|
|
17
|
+
compareAndSwap(expectedVersion: number, next: SealedSyncState, authority?: SyncOperationAuthority): Promise<boolean>;
|
|
18
|
+
}
|
|
19
|
+
/** Expected endpoint authority for a guarded host operation, outside encrypted protocol data. */
|
|
20
|
+
export interface SyncOperationAuthority {
|
|
21
|
+
vaultId: string;
|
|
22
|
+
devicePublicKey: string;
|
|
23
|
+
epoch: number;
|
|
24
|
+
membershipGeneration: number;
|
|
25
|
+
}
|
|
26
|
+
/** Immutable sealed records addressed by opaque 64-character lowercase hex keys. */
|
|
27
|
+
export interface SealedSyncRecord {
|
|
28
|
+
epoch: number;
|
|
29
|
+
ciphertext: Uint8Array;
|
|
30
|
+
}
|
|
31
|
+
/** Optional repository extension for independently encrypted, indexed local records. */
|
|
32
|
+
export interface SyncIndexedRepository extends SyncRepository {
|
|
33
|
+
readonly records: {
|
|
34
|
+
get(key: string): Promise<SealedSyncRecord | null>;
|
|
35
|
+
/** False preserves the existing record unchanged, including concurrent insertion. */
|
|
36
|
+
putIfAbsent(key: string, record: SealedSyncRecord): Promise<boolean>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Captured by the owning host; a missing file is represented by null. */
|
|
40
|
+
export interface SyncFileGeneration {
|
|
41
|
+
fileId: string;
|
|
42
|
+
contentGeneration: number;
|
|
43
|
+
hostFileId?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface SyncFileMutation {
|
|
46
|
+
id: string;
|
|
47
|
+
kind: "write" | "remove";
|
|
48
|
+
path: string;
|
|
49
|
+
expected: SyncFileGeneration | null;
|
|
50
|
+
fileId: string;
|
|
51
|
+
}
|
|
52
|
+
/** Capture only protocol-neutral mutation data; file handles may carry read functions. */
|
|
53
|
+
export declare function snapshotSyncFileMutation(input: SyncFileMutation): SyncFileMutation;
|
|
54
|
+
export interface SyncFileMutationReceipt {
|
|
55
|
+
operation: SyncFileMutation;
|
|
56
|
+
result: SyncFileGeneration | null;
|
|
57
|
+
}
|
|
58
|
+
/** The host journals immutable operation IDs and verifies guards at mutation. */
|
|
59
|
+
export interface SyncFileMutationAccess {
|
|
60
|
+
receipt(operationId: string, authority?: SyncOperationAuthority): Promise<SyncFileMutationReceipt | null>;
|
|
61
|
+
mutate(operation: SyncFileMutation, source?: ReadableStream<Uint8Array>, authority?: SyncOperationAuthority): Promise<SyncFileMutationReceipt>;
|
|
62
|
+
}
|
|
63
|
+
export interface SyncFileDescriptor extends SyncFileGeneration {
|
|
64
|
+
path: string;
|
|
65
|
+
}
|
|
66
|
+
export interface SyncGuardedFile extends SyncFileDescriptor {
|
|
67
|
+
openRead(): Promise<ReadableStream<Uint8Array>>;
|
|
68
|
+
}
|
|
69
|
+
/** Captured working files and durable encrypted state owned by the current host. */
|
|
70
|
+
export interface SyncLocalHostCapabilities {
|
|
71
|
+
list(): Promise<SyncGuardedFile[]>;
|
|
72
|
+
readonly repository: SyncRepository;
|
|
73
|
+
readonly apply: SyncFileMutationAccess;
|
|
74
|
+
}
|
|
75
|
+
/** Trusted host adapter. Neither this controller nor storage paths enter plugin facades. */
|
|
76
|
+
export interface SyncHostLocalProvider {
|
|
77
|
+
forRuntime(runtimeId: string): SyncLocalHostCapabilities;
|
|
78
|
+
revokeRuntime(runtimeId: string): void | Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
/** The owning host protects editor state for the entire callback, including recovery. */
|
|
81
|
+
export type SyncLocalApplyGuard = <T>(path: string | undefined, operation: () => Promise<T>) => Promise<T>;
|
|
82
|
+
/** Minimal opaque crypto boundary; implementations retain all secret material. */
|
|
83
|
+
export interface SyncVaultStateCrypto {
|
|
84
|
+
status(): Promise<{
|
|
85
|
+
state: "unenrolled" | "locked" | "unlocked";
|
|
86
|
+
suite: "lapis-sync-v1-xchacha20poly1305-schnorr";
|
|
87
|
+
vaultId?: string;
|
|
88
|
+
devicePublicKey?: string;
|
|
89
|
+
deviceState?: "active" | "revoked" | "recovery-pending";
|
|
90
|
+
currentEpoch?: number;
|
|
91
|
+
membershipGeneration?: number;
|
|
92
|
+
}>;
|
|
93
|
+
encryptObject(input: {
|
|
94
|
+
epoch: number;
|
|
95
|
+
type: "local-state";
|
|
96
|
+
plaintext: Uint8Array;
|
|
97
|
+
}): Promise<{
|
|
98
|
+
bytes: Uint8Array;
|
|
99
|
+
objectHash: string;
|
|
100
|
+
}>;
|
|
101
|
+
decryptObject(input: {
|
|
102
|
+
epoch: number;
|
|
103
|
+
type: "local-state";
|
|
104
|
+
envelope: Uint8Array;
|
|
105
|
+
}): Promise<Uint8Array>;
|
|
106
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Capture only protocol-neutral mutation data; file handles may carry read functions. */
|
|
2
|
+
export function snapshotSyncFileMutation(input) {
|
|
3
|
+
return {
|
|
4
|
+
id: input.id,
|
|
5
|
+
kind: input.kind,
|
|
6
|
+
path: input.path,
|
|
7
|
+
fileId: input.fileId,
|
|
8
|
+
expected: input.expected === null
|
|
9
|
+
? null
|
|
10
|
+
: {
|
|
11
|
+
fileId: input.expected.fileId,
|
|
12
|
+
contentGeneration: input.expected.contentGeneration,
|
|
13
|
+
...(input.expected.hostFileId !== undefined
|
|
14
|
+
? { hostFileId: input.expected.hostFileId }
|
|
15
|
+
: {}),
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { App } from "./context.svelte";
|
|
2
|
+
import { TFile } from "./storage/fs";
|
|
3
|
+
/** Project into existing owned views; no replacement editor or persistence path. */
|
|
4
|
+
export declare function projectVaultChange(app: App, event: string, file: TFile, context: Record<string, unknown>, current: () => boolean): Promise<void>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { TFile } from "./storage/fs";
|
|
2
|
+
import { FileView } from "./view.svelte";
|
|
3
|
+
/** Project into existing owned views; no replacement editor or persistence path. */
|
|
4
|
+
export async function projectVaultChange(app, event, file, context, current) {
|
|
5
|
+
if (!current())
|
|
6
|
+
return;
|
|
7
|
+
if (context.external === true)
|
|
8
|
+
app.workspace.trigger("file-change", file, event);
|
|
9
|
+
if (event !== "rename" && event !== "delete")
|
|
10
|
+
return;
|
|
11
|
+
const oldPath = event === "rename" ? String(context.oldPath) : file.path;
|
|
12
|
+
if (app.fileEditingSessions.hasDirty(oldPath)) {
|
|
13
|
+
app.notifications.notify({
|
|
14
|
+
id: `vault-file-change:${oldPath}`,
|
|
15
|
+
title: "File changed on disk",
|
|
16
|
+
message: event === "rename"
|
|
17
|
+
? `${oldPath} moved to ${file.path}. Local edits remain in the original view for recovery.`
|
|
18
|
+
: `${oldPath} was removed on disk. Local edits remain in the original view for recovery.`,
|
|
19
|
+
severity: "warning",
|
|
20
|
+
source: "vault",
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const leaves = [];
|
|
25
|
+
app.workspace.iterateAllLeaves((leaf) => {
|
|
26
|
+
if (leaf.view instanceof FileView && leaf.view.file?.path === oldPath)
|
|
27
|
+
leaves.push(leaf);
|
|
28
|
+
});
|
|
29
|
+
for (const leaf of leaves) {
|
|
30
|
+
if (!current() ||
|
|
31
|
+
!(leaf.view instanceof FileView) ||
|
|
32
|
+
leaf.view.file?.path !== oldPath)
|
|
33
|
+
continue;
|
|
34
|
+
if (event === "rename")
|
|
35
|
+
await leaf.view.onRename(file.copy());
|
|
36
|
+
else
|
|
37
|
+
await leaf.setViewState({ type: "empty", state: {} });
|
|
38
|
+
if (current())
|
|
39
|
+
app.workspace.requestSaveLayout();
|
|
40
|
+
}
|
|
41
|
+
if (current())
|
|
42
|
+
app.workspace.reconcileFileReference(oldPath, event === "rename" ? file : null);
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { App } from "./context.svelte";
|
|
2
|
+
/** One App owns watching, reconciliation and retirement for its live Vault. */
|
|
3
|
+
export declare class VaultChangeService {
|
|
4
|
+
private readonly app;
|
|
5
|
+
private watcher;
|
|
6
|
+
private starting;
|
|
7
|
+
private stopping;
|
|
8
|
+
private processing;
|
|
9
|
+
private timer;
|
|
10
|
+
private generation;
|
|
11
|
+
private active;
|
|
12
|
+
private registered;
|
|
13
|
+
private dirty;
|
|
14
|
+
private uncertain;
|
|
15
|
+
private failures;
|
|
16
|
+
private readonly hints;
|
|
17
|
+
private stopProjection;
|
|
18
|
+
private readonly projections;
|
|
19
|
+
constructor(app: App);
|
|
20
|
+
start(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Retire callbacks synchronously, then drain registration, native IO and
|
|
23
|
+
* scans.
|
|
24
|
+
*/
|
|
25
|
+
stop(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Flush pending hints now; explicit calls also retry a previously failed
|
|
28
|
+
* scan.
|
|
29
|
+
*/
|
|
30
|
+
settle(): Promise<void>;
|
|
31
|
+
private live;
|
|
32
|
+
private clearTimer;
|
|
33
|
+
private report;
|
|
34
|
+
private schedule;
|
|
35
|
+
private changed;
|
|
36
|
+
private drain;
|
|
37
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { DirectoryWatcher, TFile, } from "./storage/fs";
|
|
2
|
+
import { projectVaultChange } from "./vault-change-views";
|
|
3
|
+
/** One App owns watching, reconciliation and retirement for its live Vault. */
|
|
4
|
+
export class VaultChangeService {
|
|
5
|
+
app;
|
|
6
|
+
watcher;
|
|
7
|
+
starting;
|
|
8
|
+
stopping;
|
|
9
|
+
processing;
|
|
10
|
+
timer;
|
|
11
|
+
generation = 0;
|
|
12
|
+
active = false;
|
|
13
|
+
registered = false;
|
|
14
|
+
dirty = false;
|
|
15
|
+
uncertain = false;
|
|
16
|
+
failures = 0;
|
|
17
|
+
hints = new Map();
|
|
18
|
+
stopProjection;
|
|
19
|
+
projections = new Set();
|
|
20
|
+
constructor(app) {
|
|
21
|
+
this.app = app;
|
|
22
|
+
}
|
|
23
|
+
start() {
|
|
24
|
+
if (this.starting)
|
|
25
|
+
return this.starting;
|
|
26
|
+
if (this.active)
|
|
27
|
+
return this.settle();
|
|
28
|
+
const run = async () => {
|
|
29
|
+
const retiring = this.stop();
|
|
30
|
+
const requested = this.generation;
|
|
31
|
+
await retiring;
|
|
32
|
+
if (this.generation !== requested)
|
|
33
|
+
return;
|
|
34
|
+
const generation = ++this.generation;
|
|
35
|
+
this.active = true;
|
|
36
|
+
this.dirty = true;
|
|
37
|
+
const watcher = new DirectoryWatcher(this.app.vault.adapter);
|
|
38
|
+
this.watcher = watcher;
|
|
39
|
+
const projection = this.app.vault.on("all", (event, file, context) => {
|
|
40
|
+
if (!this.live(generation) || !(file instanceof TFile))
|
|
41
|
+
return;
|
|
42
|
+
const snapshot = file.copy(), details = { ...context };
|
|
43
|
+
const pending = this.app.fileEditingSessions.track(() => this.app.fileEditingSessions.afterMutations(() => projectVaultChange(this.app, event, snapshot, details, () => this.live(generation))));
|
|
44
|
+
this.projections.add(pending);
|
|
45
|
+
void pending
|
|
46
|
+
.catch((error) => this.report(error))
|
|
47
|
+
.finally(() => this.projections.delete(pending));
|
|
48
|
+
});
|
|
49
|
+
this.stopProjection = () => this.app.vault.offref(projection);
|
|
50
|
+
watcher.on("all", (event) => {
|
|
51
|
+
if (this.live(generation))
|
|
52
|
+
this.changed(event, generation);
|
|
53
|
+
});
|
|
54
|
+
const subscription = watcher.watch("/", { recursive: true });
|
|
55
|
+
await subscription.ready;
|
|
56
|
+
if (!this.live(generation))
|
|
57
|
+
return;
|
|
58
|
+
this.registered = true;
|
|
59
|
+
await this.drain(generation);
|
|
60
|
+
};
|
|
61
|
+
const pending = run()
|
|
62
|
+
.catch(async (error) => {
|
|
63
|
+
this.active = false;
|
|
64
|
+
this.registered = false;
|
|
65
|
+
++this.generation;
|
|
66
|
+
this.clearTimer();
|
|
67
|
+
this.stopProjection?.();
|
|
68
|
+
this.stopProjection = undefined;
|
|
69
|
+
try {
|
|
70
|
+
await this.watcher?.close();
|
|
71
|
+
}
|
|
72
|
+
catch (cleanup) {
|
|
73
|
+
throw new AggregateError([error, cleanup], "Vault watch startup and cleanup failed");
|
|
74
|
+
}
|
|
75
|
+
this.watcher = undefined;
|
|
76
|
+
throw error;
|
|
77
|
+
})
|
|
78
|
+
.finally(() => {
|
|
79
|
+
if (this.starting === pending)
|
|
80
|
+
this.starting = undefined;
|
|
81
|
+
});
|
|
82
|
+
this.starting = pending;
|
|
83
|
+
return pending;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Retire callbacks synchronously, then drain registration, native IO and
|
|
87
|
+
* scans.
|
|
88
|
+
*/
|
|
89
|
+
stop() {
|
|
90
|
+
this.active = false;
|
|
91
|
+
this.registered = false;
|
|
92
|
+
++this.generation;
|
|
93
|
+
this.clearTimer();
|
|
94
|
+
this.stopProjection?.();
|
|
95
|
+
this.stopProjection = undefined;
|
|
96
|
+
this.hints.clear();
|
|
97
|
+
this.dirty = false;
|
|
98
|
+
this.uncertain = false;
|
|
99
|
+
if (this.stopping)
|
|
100
|
+
return this.stopping;
|
|
101
|
+
const watcher = this.watcher;
|
|
102
|
+
const pending = Promise.allSettled([
|
|
103
|
+
watcher?.close(),
|
|
104
|
+
this.processing,
|
|
105
|
+
this.starting,
|
|
106
|
+
...this.projections,
|
|
107
|
+
])
|
|
108
|
+
.then((results) => {
|
|
109
|
+
const errors = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
110
|
+
if (errors.length)
|
|
111
|
+
throw new AggregateError(errors, "Vault watch cleanup failed");
|
|
112
|
+
if (this.watcher === watcher)
|
|
113
|
+
this.watcher = undefined;
|
|
114
|
+
})
|
|
115
|
+
.finally(() => {
|
|
116
|
+
if (this.stopping === pending)
|
|
117
|
+
this.stopping = undefined;
|
|
118
|
+
});
|
|
119
|
+
this.stopping = pending;
|
|
120
|
+
return pending;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Flush pending hints now; explicit calls also retry a previously failed
|
|
124
|
+
* scan.
|
|
125
|
+
*/
|
|
126
|
+
async settle() {
|
|
127
|
+
await this.starting;
|
|
128
|
+
this.clearTimer();
|
|
129
|
+
if (this.active && this.registered) {
|
|
130
|
+
await this.drain(this.generation);
|
|
131
|
+
while (this.projections.size)
|
|
132
|
+
await Promise.all([...this.projections]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
live(generation) {
|
|
136
|
+
return this.active && this.generation === generation;
|
|
137
|
+
}
|
|
138
|
+
clearTimer() {
|
|
139
|
+
if (this.timer)
|
|
140
|
+
clearTimeout(this.timer);
|
|
141
|
+
this.timer = undefined;
|
|
142
|
+
}
|
|
143
|
+
report(error) {
|
|
144
|
+
this.app.notifications.notify({
|
|
145
|
+
id: "vault-watch-reconciliation",
|
|
146
|
+
title: "File changes need attention",
|
|
147
|
+
message: error instanceof Error ? error.message : String(error),
|
|
148
|
+
severity: "error",
|
|
149
|
+
source: "vault",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
schedule(generation, delay = 40) {
|
|
153
|
+
if (!this.registered || this.timer || !this.live(generation))
|
|
154
|
+
return;
|
|
155
|
+
this.timer = setTimeout(() => {
|
|
156
|
+
this.timer = undefined;
|
|
157
|
+
void this.drain(generation).catch(() => { });
|
|
158
|
+
}, delay);
|
|
159
|
+
}
|
|
160
|
+
changed(event, generation) {
|
|
161
|
+
this.dirty = true;
|
|
162
|
+
if (event.type === "error") {
|
|
163
|
+
this.uncertain = true;
|
|
164
|
+
this.hints.clear();
|
|
165
|
+
this.report(event.error);
|
|
166
|
+
}
|
|
167
|
+
else if (!this.uncertain) {
|
|
168
|
+
const previous = event.previousPath
|
|
169
|
+
? this.hints.get(event.previousPath)
|
|
170
|
+
: this.hints.get(event.path);
|
|
171
|
+
const hint = {
|
|
172
|
+
...event,
|
|
173
|
+
previousPath: previous?.previousPath ?? event.previousPath,
|
|
174
|
+
};
|
|
175
|
+
if (event.previousPath)
|
|
176
|
+
this.hints.delete(event.previousPath);
|
|
177
|
+
this.hints.set(event.path, hint);
|
|
178
|
+
const bytes = [...this.hints.values()].reduce((sum, value) => sum +
|
|
179
|
+
new TextEncoder().encode(value.path + (value.previousPath ?? ""))
|
|
180
|
+
.byteLength +
|
|
181
|
+
96, 0);
|
|
182
|
+
if (event.path === "/" ||
|
|
183
|
+
!event.path ||
|
|
184
|
+
this.hints.size > 2048 ||
|
|
185
|
+
bytes > 128 * 1024) {
|
|
186
|
+
this.uncertain = true;
|
|
187
|
+
this.hints.clear();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this.schedule(generation);
|
|
191
|
+
}
|
|
192
|
+
drain(generation) {
|
|
193
|
+
if (this.processing)
|
|
194
|
+
return this.processing;
|
|
195
|
+
const run = async () => {
|
|
196
|
+
while (this.live(generation) && this.dirty) {
|
|
197
|
+
const hints = this.uncertain ? [] : [...this.hints.values()];
|
|
198
|
+
this.hints.clear();
|
|
199
|
+
this.dirty = false;
|
|
200
|
+
this.uncertain = false;
|
|
201
|
+
try {
|
|
202
|
+
await this.app.vault.reconcile(hints, () => this.live(generation), {
|
|
203
|
+
external: true,
|
|
204
|
+
});
|
|
205
|
+
while (this.projections.size)
|
|
206
|
+
await Promise.all([...this.projections]);
|
|
207
|
+
this.failures = 0;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (this.live(generation)) {
|
|
211
|
+
this.dirty = true;
|
|
212
|
+
this.uncertain = true;
|
|
213
|
+
this.report(error);
|
|
214
|
+
if (++this.failures <= 4)
|
|
215
|
+
this.schedule(generation, 250 * 2 ** (this.failures - 1));
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const pending = run().finally(() => {
|
|
222
|
+
if (this.processing === pending)
|
|
223
|
+
this.processing = undefined;
|
|
224
|
+
});
|
|
225
|
+
this.processing = pending;
|
|
226
|
+
return pending;
|
|
227
|
+
}
|
|
228
|
+
}
|