@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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { snapshotSyncFileMutation } from "./sync-repository";
|
|
2
|
+
const FILE_GRANTS = [
|
|
3
|
+
"crypto:nostr-sync",
|
|
4
|
+
"vault:stream",
|
|
5
|
+
"metadata:identity",
|
|
6
|
+
];
|
|
7
|
+
const STORE_GRANTS = ["crypto:nostr-sync", "plugin:generated-state"];
|
|
8
|
+
/** Grant-checked lifetime boundary; providers retain all platform-specific authority. */
|
|
9
|
+
export class ScopedSyncLocalRuntime {
|
|
10
|
+
options;
|
|
11
|
+
#active = true;
|
|
12
|
+
#provider;
|
|
13
|
+
#capabilities;
|
|
14
|
+
#selectionError;
|
|
15
|
+
#selected = false;
|
|
16
|
+
#closing;
|
|
17
|
+
#flights = new Set();
|
|
18
|
+
#streams = new Set();
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
}
|
|
22
|
+
capabilities = {
|
|
23
|
+
list: () => this.#invoke(FILE_GRANTS, async (local) => {
|
|
24
|
+
const files = await local.list();
|
|
25
|
+
return files.map((file) => ({
|
|
26
|
+
path: file.path,
|
|
27
|
+
fileId: file.fileId,
|
|
28
|
+
hostFileId: file.hostFileId,
|
|
29
|
+
contentGeneration: file.contentGeneration,
|
|
30
|
+
openRead: () => this.#invoke(FILE_GRANTS, async () => (await this.#stream(await file.openRead(), FILE_GRANTS)).stream),
|
|
31
|
+
}));
|
|
32
|
+
}),
|
|
33
|
+
repository: {
|
|
34
|
+
readState: () => this.#invoke(STORE_GRANTS, (local) => local.repository.readState()),
|
|
35
|
+
compareAndSwap: (expectedVersion, next, authority) => {
|
|
36
|
+
const captured = structuredClone(next);
|
|
37
|
+
const fence = authority ? structuredClone(authority) : undefined;
|
|
38
|
+
return this.#invoke(STORE_GRANTS, (local) => fence
|
|
39
|
+
? local.repository.compareAndSwap(expectedVersion, captured, fence)
|
|
40
|
+
: local.repository.compareAndSwap(expectedVersion, captured));
|
|
41
|
+
},
|
|
42
|
+
blobs: {
|
|
43
|
+
has: (hash) => this.#invoke(STORE_GRANTS, (local) => local.repository.blobs.has(hash)),
|
|
44
|
+
get: (hash) => this.#invoke(STORE_GRANTS, (local) => local.repository.blobs.get(hash)),
|
|
45
|
+
put: (bytes) => {
|
|
46
|
+
const captured = Uint8Array.from(bytes);
|
|
47
|
+
return this.#invoke(STORE_GRANTS, (local) => local.repository.blobs.put(captured));
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
apply: {
|
|
52
|
+
receipt: (id, authority) => {
|
|
53
|
+
const fence = authority ? structuredClone(authority) : undefined;
|
|
54
|
+
return this.#apply(undefined, (local) => fence ? local.apply.receipt(id, fence) : local.apply.receipt(id));
|
|
55
|
+
},
|
|
56
|
+
mutate: (operation, source, authority) => {
|
|
57
|
+
return this.#track((async () => {
|
|
58
|
+
let guarded;
|
|
59
|
+
try {
|
|
60
|
+
const captured = snapshotSyncFileMutation(operation);
|
|
61
|
+
const fence = authority ? structuredClone(authority) : undefined;
|
|
62
|
+
guarded = source
|
|
63
|
+
? await this.#stream(source, FILE_GRANTS)
|
|
64
|
+
: undefined;
|
|
65
|
+
return await this.#apply(captured.path, async (local) => {
|
|
66
|
+
try {
|
|
67
|
+
this.#assert([...FILE_GRANTS, "plugin:generated-state"]);
|
|
68
|
+
return await (fence
|
|
69
|
+
? local.apply.mutate(captured, guarded?.stream, fence)
|
|
70
|
+
: local.apply.mutate(captured, guarded?.stream));
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await guarded?.close();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (guarded)
|
|
79
|
+
await guarded.close();
|
|
80
|
+
else if (source && !source.locked)
|
|
81
|
+
await source.cancel();
|
|
82
|
+
}
|
|
83
|
+
})());
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
#assert(grants) {
|
|
88
|
+
if (!this.#active)
|
|
89
|
+
throw new Error("Local Sync runtime is revoked");
|
|
90
|
+
for (const grant of grants)
|
|
91
|
+
this.options.assertGranted(grant);
|
|
92
|
+
}
|
|
93
|
+
#local() {
|
|
94
|
+
if (!this.#selected) {
|
|
95
|
+
this.#selected = true;
|
|
96
|
+
try {
|
|
97
|
+
const provider = this.options.selectProvider();
|
|
98
|
+
if (!provider)
|
|
99
|
+
throw new Error("Local Sync host capability is unavailable");
|
|
100
|
+
if (typeof provider.forRuntime !== "function" ||
|
|
101
|
+
typeof provider.revokeRuntime !== "function")
|
|
102
|
+
throw new Error("Invalid local Sync provider");
|
|
103
|
+
this.#provider = provider;
|
|
104
|
+
this.#capabilities = provider.forRuntime(this.options.runtimeId);
|
|
105
|
+
if (!this.#capabilities || typeof this.#capabilities !== "object")
|
|
106
|
+
throw new Error("Invalid local Sync provider capabilities");
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
this.#selectionError =
|
|
110
|
+
error instanceof Error
|
|
111
|
+
? error
|
|
112
|
+
: new Error("Local Sync provider selection failed");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (this.#selectionError)
|
|
116
|
+
throw this.#selectionError;
|
|
117
|
+
return this.#capabilities;
|
|
118
|
+
}
|
|
119
|
+
#invoke(grants, operation) {
|
|
120
|
+
const flight = (async () => {
|
|
121
|
+
this.#assert(grants);
|
|
122
|
+
const result = await operation(this.#local());
|
|
123
|
+
this.#assert(grants);
|
|
124
|
+
return result;
|
|
125
|
+
})();
|
|
126
|
+
return this.#track(flight);
|
|
127
|
+
}
|
|
128
|
+
#track(flight) {
|
|
129
|
+
this.#flights.add(flight);
|
|
130
|
+
void flight.then(() => this.#flights.delete(flight), () => this.#flights.delete(flight));
|
|
131
|
+
return flight;
|
|
132
|
+
}
|
|
133
|
+
async #apply(path, operation) {
|
|
134
|
+
this.#assert([...FILE_GRANTS, "plugin:generated-state"]);
|
|
135
|
+
const guard = this.options.guardApply;
|
|
136
|
+
if (!guard)
|
|
137
|
+
throw new Error("Local Sync host apply guard is unavailable");
|
|
138
|
+
return this.#invoke([...FILE_GRANTS, "plugin:generated-state"], async (local) => {
|
|
139
|
+
return guard(path, async () => {
|
|
140
|
+
this.#assert([...FILE_GRANTS, "plugin:generated-state"]);
|
|
141
|
+
return operation(local);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async #stream(source, grants) {
|
|
146
|
+
try {
|
|
147
|
+
this.#assert(grants);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
await source.cancel(error);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
const reader = source.getReader();
|
|
154
|
+
let terminal = false;
|
|
155
|
+
let stopping;
|
|
156
|
+
let controller;
|
|
157
|
+
const release = () => {
|
|
158
|
+
this.#streams.delete(stop);
|
|
159
|
+
reader.releaseLock();
|
|
160
|
+
};
|
|
161
|
+
const cancel = (reason) => {
|
|
162
|
+
if (stopping)
|
|
163
|
+
return stopping;
|
|
164
|
+
if (terminal)
|
|
165
|
+
return Promise.resolve();
|
|
166
|
+
terminal = true;
|
|
167
|
+
stopping = reader.cancel(reason).finally(release);
|
|
168
|
+
return stopping;
|
|
169
|
+
};
|
|
170
|
+
const stop = (reason = new Error("Local Sync runtime is revoked")) => {
|
|
171
|
+
if (!terminal)
|
|
172
|
+
controller.error(reason);
|
|
173
|
+
return cancel(reason);
|
|
174
|
+
};
|
|
175
|
+
this.#streams.add(stop);
|
|
176
|
+
const stream = new ReadableStream({
|
|
177
|
+
start(value) {
|
|
178
|
+
controller = value;
|
|
179
|
+
},
|
|
180
|
+
pull: async () => {
|
|
181
|
+
if (terminal)
|
|
182
|
+
return;
|
|
183
|
+
try {
|
|
184
|
+
this.#assert(grants);
|
|
185
|
+
const result = await reader.read();
|
|
186
|
+
this.#assert(grants);
|
|
187
|
+
if (terminal)
|
|
188
|
+
return;
|
|
189
|
+
if (result.done) {
|
|
190
|
+
terminal = true;
|
|
191
|
+
release();
|
|
192
|
+
controller.close();
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
if (!(result.value instanceof Uint8Array))
|
|
196
|
+
throw new Error("Invalid local Sync stream chunk");
|
|
197
|
+
controller.enqueue(result.value);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (!terminal)
|
|
202
|
+
controller.error(error);
|
|
203
|
+
await cancel(error);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
cancel,
|
|
207
|
+
}, { highWaterMark: 0 });
|
|
208
|
+
return {
|
|
209
|
+
stream,
|
|
210
|
+
close: () => stop(new Error("Local Sync operation has completed")),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
revoke() {
|
|
214
|
+
if (this.#closing)
|
|
215
|
+
return this.#closing;
|
|
216
|
+
this.#active = false;
|
|
217
|
+
this.#closing = (async () => {
|
|
218
|
+
const streams = await Promise.allSettled([...this.#streams].map((stop) => stop()));
|
|
219
|
+
await Promise.allSettled([...this.#flights]);
|
|
220
|
+
// Even failed cancellation must release the platform provider.
|
|
221
|
+
await this.#provider?.revokeRuntime(this.options.runtimeId);
|
|
222
|
+
const failure = streams.find((result) => result.status === "rejected");
|
|
223
|
+
if (failure?.status === "rejected")
|
|
224
|
+
throw failure.reason;
|
|
225
|
+
})();
|
|
226
|
+
return this.#closing;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Event as NostrEvent } from "nostr-tools/pure";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type { HostMetadataStore } from "./metadata-store";
|
|
3
|
+
/** Host-owned persistence for wrapped keys and network grants, outside disposable indexes. */
|
|
4
|
+
export type SyncSecurityStore = HostMetadataStore;
|
|
5
|
+
export type SyncCryptoObjectType = "file-content" | "file-manifest" | "tree-manifest" | "revision-payload" | "revision-archive" | "head-payload" | "membership-payload" | "local-state";
|
|
4
6
|
export type SyncDeviceRole = "owner" | "member";
|
|
5
7
|
export type SyncIdentityState = "setup-required" | "active" | "rotation-pending";
|
|
6
8
|
export interface SyncIdentityHistoryEntry {
|
|
@@ -64,6 +66,8 @@ export interface SyncCryptoStatus {
|
|
|
64
66
|
role?: SyncDeviceRole;
|
|
65
67
|
deviceState?: "active" | "revoked" | "recovery-pending";
|
|
66
68
|
currentEpoch?: number;
|
|
69
|
+
/** Installed content-key epoch numbers, sorted and copied; present only while unlocked. */
|
|
70
|
+
installedEpochs?: number[];
|
|
67
71
|
membershipGeneration?: number;
|
|
68
72
|
canManageDevices?: boolean;
|
|
69
73
|
syncIdentity?: SyncIdentityPublicStatus;
|
|
@@ -144,20 +148,31 @@ export interface SyncPairingActionResult {
|
|
|
144
148
|
outboundEvents: NostrEvent[];
|
|
145
149
|
/** Public transport endpoints for this bounded pairing session. */
|
|
146
150
|
relayUrls: string[];
|
|
151
|
+
/** Omitted by older hosts, whose invitations use relay delivery. */
|
|
152
|
+
delivery?: SyncPairingDelivery;
|
|
147
153
|
invitationUri?: string;
|
|
148
154
|
membershipEvent?: NostrEvent;
|
|
149
155
|
/** Bounded plugin data recovered only from the encrypted pairing transfer. */
|
|
150
156
|
metadata?: unknown;
|
|
151
157
|
}
|
|
158
|
+
export type SyncPairingDelivery = "relay" | "host";
|
|
159
|
+
export type SyncPairingOwnerRequest = {
|
|
160
|
+
cutoffHeads?: Record<string, string>;
|
|
161
|
+
metadata?: unknown;
|
|
162
|
+
} & ({
|
|
163
|
+
delivery?: "relay";
|
|
164
|
+
relayUrls: string[];
|
|
165
|
+
} | {
|
|
166
|
+
delivery: "host";
|
|
167
|
+
relayUrls?: never;
|
|
168
|
+
});
|
|
152
169
|
export interface SyncPairingCapability {
|
|
153
|
-
beginOwner(input:
|
|
154
|
-
relayUrls: string[];
|
|
155
|
-
cutoffHeads?: Record<string, string>;
|
|
156
|
-
metadata?: unknown;
|
|
157
|
-
}): Promise<SyncPairingActionResult>;
|
|
170
|
+
beginOwner(input: SyncPairingOwnerRequest): Promise<SyncPairingActionResult>;
|
|
158
171
|
acceptInvite(input: {
|
|
159
172
|
invitationUri: string;
|
|
160
173
|
deviceAlias: string;
|
|
174
|
+
/** Host delivery must be selected explicitly for an invitation with no relay addresses. */
|
|
175
|
+
delivery?: SyncPairingDelivery;
|
|
161
176
|
}): Promise<SyncPairingActionResult>;
|
|
162
177
|
ingest(input: {
|
|
163
178
|
sessionId: string;
|
|
@@ -257,9 +272,9 @@ export interface SyncHostSecurityOptions {
|
|
|
257
272
|
}
|
|
258
273
|
export declare class SyncHostSecurityService {
|
|
259
274
|
#private;
|
|
260
|
-
readonly database:
|
|
275
|
+
readonly database: SyncSecurityStore;
|
|
261
276
|
readonly assertRuntime: (pluginId: string, runtimeId: string) => void;
|
|
262
|
-
constructor(database:
|
|
277
|
+
constructor(database: SyncSecurityStore, assertRuntime: (pluginId: string, runtimeId: string) => void, options?: SyncHostSecurityOptions);
|
|
263
278
|
enroll(pluginId: string, passphrase: string, request?: SyncCryptoEnrollmentRequest): Promise<SyncCryptoStatus>;
|
|
264
279
|
enrollWithWrappingKey(pluginId: string, wrappingKey: Uint8Array, descriptor: Extract<SyncWrappingKeyDescriptor, {
|
|
265
280
|
name: "external";
|
|
@@ -28,6 +28,7 @@ function typeTag(type) {
|
|
|
28
28
|
"revision-archive": 5,
|
|
29
29
|
"head-payload": 6,
|
|
30
30
|
"membership-payload": 7,
|
|
31
|
+
"local-state": 8,
|
|
31
32
|
}[type];
|
|
32
33
|
}
|
|
33
34
|
function requireCryptoSize(bytes) {
|
|
@@ -98,12 +99,21 @@ function requirePublicKey(value, label = "Device public key") {
|
|
|
98
99
|
}
|
|
99
100
|
return value;
|
|
100
101
|
}
|
|
102
|
+
function requirePairingDelivery(value) {
|
|
103
|
+
if (value === undefined)
|
|
104
|
+
return "relay";
|
|
105
|
+
if (value === "relay" || value === "host")
|
|
106
|
+
return value;
|
|
107
|
+
throw new Error("Unsupported pairing delivery mode");
|
|
108
|
+
}
|
|
101
109
|
function requireRelayUrls(values) {
|
|
102
|
-
if (values.length < 1 || values.length > 8) {
|
|
110
|
+
if (!Array.isArray(values) || values.length < 1 || values.length > 8) {
|
|
103
111
|
throw new Error("Pairing requires between one and eight relay URLs");
|
|
104
112
|
}
|
|
105
113
|
const result = [];
|
|
106
114
|
for (const value of values) {
|
|
115
|
+
if (typeof value !== "string")
|
|
116
|
+
throw new Error("Pairing relay URLs must be strings");
|
|
107
117
|
const url = new URL(value);
|
|
108
118
|
if (!["wss:", "ws:"].includes(url.protocol)) {
|
|
109
119
|
throw new Error("Pairing relays must use wss or loopback ws");
|
|
@@ -286,6 +296,9 @@ export class SyncHostSecurityService {
|
|
|
286
296
|
constructor(database, assertRuntime, options = {}) {
|
|
287
297
|
this.database = database;
|
|
288
298
|
this.assertRuntime = assertRuntime;
|
|
299
|
+
if (!database ||
|
|
300
|
+
["getMeta", "setMeta", "transactMeta"].some((name) => typeof database[name] !== "function"))
|
|
301
|
+
throw new Error("Invalid Sync security store");
|
|
289
302
|
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
290
303
|
this.#WebSocket = options.WebSocket ?? globalThis.WebSocket;
|
|
291
304
|
this.#argon2 = {
|
|
@@ -387,12 +400,19 @@ export class SyncHostSecurityService {
|
|
|
387
400
|
const keys = pending?.material
|
|
388
401
|
? this.#materialToUnlocked(pending.material, wrappingKey, descriptor)
|
|
389
402
|
: this.#createOwnerKeys(requireAlias(request.deviceAlias ?? "This device"), wrappingKey, descriptor);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
403
|
+
let applied;
|
|
404
|
+
try {
|
|
405
|
+
const envelope = this.#sealEnvelope(pluginId, keys);
|
|
406
|
+
applied = (await this.database.transactMeta({
|
|
407
|
+
checks: [{ key: envelopeKey(pluginId), exists: false }],
|
|
408
|
+
mutations: [{ key: envelopeKey(pluginId), value: envelope }],
|
|
409
|
+
})).applied;
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
this.#destroyKeys(keys);
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
if (!applied) {
|
|
396
416
|
this.#destroyKeys(keys);
|
|
397
417
|
throw new Error(`Sync crypto enrollment changed concurrently for ${pluginId}`);
|
|
398
418
|
}
|
|
@@ -708,6 +728,7 @@ export class SyncHostSecurityService {
|
|
|
708
728
|
role: unlocked.role,
|
|
709
729
|
deviceState: this.#deviceState(unlocked),
|
|
710
730
|
currentEpoch: unlocked.currentEpoch,
|
|
731
|
+
installedEpochs: [...unlocked.vaultKeys.keys()].sort((left, right) => left - right),
|
|
711
732
|
membershipGeneration: unlocked.membershipGeneration,
|
|
712
733
|
canManageDevices: unlocked.role === "owner",
|
|
713
734
|
syncIdentity: this.#syncIdentityStatus(unlocked),
|
|
@@ -1059,20 +1080,24 @@ export class SyncHostSecurityService {
|
|
|
1059
1080
|
session: this.#pairingSnapshot(session),
|
|
1060
1081
|
outboundEvents,
|
|
1061
1082
|
relayUrls: [...session.relays],
|
|
1083
|
+
delivery: session.delivery,
|
|
1062
1084
|
...extras,
|
|
1063
1085
|
});
|
|
1064
1086
|
return {
|
|
1065
|
-
beginOwner: async ({ relayUrls, cutoffHeads = {}, metadata }) => {
|
|
1087
|
+
beginOwner: async ({ relayUrls, delivery, cutoffHeads = {}, metadata, }) => {
|
|
1066
1088
|
assertActive();
|
|
1067
1089
|
const keys = this.#requireUnlocked(pluginId);
|
|
1068
1090
|
if (keys.role !== "owner" || !keys.ownerSecret) {
|
|
1069
1091
|
throw new Error("Only the owner device may begin pairing");
|
|
1070
1092
|
}
|
|
1093
|
+
const selectedDelivery = requirePairingDelivery(delivery);
|
|
1094
|
+
if (selectedDelivery === "host" && relayUrls !== undefined)
|
|
1095
|
+
throw new Error("Host-delivered pairing must not specify relay URLs");
|
|
1096
|
+
const relays = selectedDelivery === "host" ? [] : requireRelayUrls(relayUrls);
|
|
1071
1097
|
const ephemeralSecret = randomBytes(KEY_LENGTH);
|
|
1072
1098
|
const sessionSecret = toBase64Url(randomBytes(KEY_LENGTH));
|
|
1073
1099
|
const sessionId = pairingSessionId(sessionSecret);
|
|
1074
1100
|
const expiresAt = Date.now() + PAIRING_TTL_MS;
|
|
1075
|
-
const relays = requireRelayUrls(relayUrls);
|
|
1076
1101
|
const session = {
|
|
1077
1102
|
pluginId,
|
|
1078
1103
|
sessionId,
|
|
@@ -1083,6 +1108,7 @@ export class SyncHostSecurityService {
|
|
|
1083
1108
|
ephemeralPublicKey: getPublicKey(ephemeralSecret),
|
|
1084
1109
|
sessionSecret,
|
|
1085
1110
|
relays,
|
|
1111
|
+
delivery: selectedDelivery,
|
|
1086
1112
|
sequence: 0,
|
|
1087
1113
|
lastReceivedSequence: 0,
|
|
1088
1114
|
localConfirmed: false,
|
|
@@ -1107,12 +1133,13 @@ export class SyncHostSecurityService {
|
|
|
1107
1133
|
}
|
|
1108
1134
|
return result(session, [], { invitationUri });
|
|
1109
1135
|
},
|
|
1110
|
-
acceptInvite: async ({ invitationUri, deviceAlias }) => {
|
|
1136
|
+
acceptInvite: async ({ invitationUri, deviceAlias, delivery }) => {
|
|
1111
1137
|
assertActive();
|
|
1112
1138
|
if ((await this.status(pluginId)).state !== "unenrolled") {
|
|
1113
1139
|
throw new Error("Pairing import requires an unenrolled sync identity");
|
|
1114
1140
|
}
|
|
1115
|
-
const
|
|
1141
|
+
const selectedDelivery = requirePairingDelivery(delivery);
|
|
1142
|
+
const invitation = this.#parsePairingInvitation(invitationUri, selectedDelivery);
|
|
1116
1143
|
if (Date.now() >= invitation.e)
|
|
1117
1144
|
throw new Error("Pairing invitation expired");
|
|
1118
1145
|
const ephemeralSecret = randomBytes(KEY_LENGTH);
|
|
@@ -1127,7 +1154,8 @@ export class SyncHostSecurityService {
|
|
|
1127
1154
|
ephemeralSecret,
|
|
1128
1155
|
ephemeralPublicKey: getPublicKey(ephemeralSecret),
|
|
1129
1156
|
sessionSecret: invitation.s,
|
|
1130
|
-
relays:
|
|
1157
|
+
relays: [...invitation.r],
|
|
1158
|
+
delivery: selectedDelivery,
|
|
1131
1159
|
sequence: 0,
|
|
1132
1160
|
lastReceivedSequence: 0,
|
|
1133
1161
|
peerEphemeralPublicKey: requirePublicKey(invitation.p, "Pairing public key"),
|
|
@@ -1717,7 +1745,7 @@ export class SyncHostSecurityService {
|
|
|
1717
1745
|
session.pendingDeviceSecret?.fill(0);
|
|
1718
1746
|
session.pendingDeviceSecret = undefined;
|
|
1719
1747
|
}
|
|
1720
|
-
#parsePairingInvitation(invitationUri) {
|
|
1748
|
+
#parsePairingInvitation(invitationUri, delivery) {
|
|
1721
1749
|
if (invitationUri.length > 2_048) {
|
|
1722
1750
|
throw new Error("Pairing invitation exceeds the app URL limit");
|
|
1723
1751
|
}
|
|
@@ -1752,7 +1780,12 @@ export class SyncHostSecurityService {
|
|
|
1752
1780
|
throw new Error("Pairing invitation data is invalid");
|
|
1753
1781
|
}
|
|
1754
1782
|
requirePublicKey(invitation.p, "Pairing public key");
|
|
1755
|
-
|
|
1783
|
+
if (delivery === "host") {
|
|
1784
|
+
if (invitation.r.length !== 0)
|
|
1785
|
+
throw new Error("Host-delivered pairing invitations must not contain relay URLs");
|
|
1786
|
+
}
|
|
1787
|
+
else
|
|
1788
|
+
invitation.r = requireRelayUrls(invitation.r);
|
|
1756
1789
|
return invitation;
|
|
1757
1790
|
}
|
|
1758
1791
|
#createPairingEvent(session, input) {
|
package/dist/sync-host.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { TFile } from "./storage/fs";
|
|
|
3
3
|
import type { VaultReadHandle } from "./storage/fs";
|
|
4
4
|
import type { Vault } from "./storage/vault.svelte";
|
|
5
5
|
import type { HostedPluginCapability } from "./plugin-capability-facade";
|
|
6
|
-
import
|
|
6
|
+
import type { SyncHostLocalProvider, SyncLocalApplyGuard, SyncLocalHostCapabilities } from "./sync-repository";
|
|
7
|
+
export type { SyncHostLocalProvider, SyncLocalApplyGuard, SyncLocalHostCapabilities, } from "./sync-repository";
|
|
8
|
+
import { type PluginNetworkCapability, type SyncCryptoCapability, type SyncCryptoEnrollmentRequest, type SyncCryptoStatus, type SyncIdentityCapability, type SyncHostSecurityOptions, type SyncMembershipCapability, type SyncPairingCapability, type SyncRecoveryCapability, type SyncSecurityStore, type SyncWrappingKeyDescriptor } from "./sync-host-security";
|
|
7
9
|
export type HostFileIdentityState = "present" | "tombstoned";
|
|
8
10
|
export type HostFileChangeKind = "create" | "modify" | "rename" | "delete";
|
|
9
11
|
export interface HostFileIdentity {
|
|
@@ -92,6 +94,8 @@ export interface PluginGeneratedState {
|
|
|
92
94
|
clear(): Promise<void>;
|
|
93
95
|
}
|
|
94
96
|
export interface SyncHostCapabilities {
|
|
97
|
+
/** Omitted by hosts without durable guarded local Sync support. */
|
|
98
|
+
readonly local?: SyncLocalHostCapabilities;
|
|
95
99
|
readonly files: {
|
|
96
100
|
getIdentity(path: string): Promise<HostFileIdentity | null>;
|
|
97
101
|
resolveIdentity(fileId: string): Promise<HostFileIdentity | null>;
|
|
@@ -133,7 +137,27 @@ export interface SyncHostCapabilities {
|
|
|
133
137
|
readonly recovery: SyncRecoveryCapability;
|
|
134
138
|
readonly generatedState: PluginGeneratedState;
|
|
135
139
|
}
|
|
140
|
+
export type SyncEndpointSecurityCapabilities = Pick<SyncHostCapabilities, "crypto" | "syncIdentity" | "pairing" | "membership" | "recovery">;
|
|
141
|
+
/** Trusted host custody. This controller is never included in plugin facades. */
|
|
142
|
+
export interface SyncHostSecurityProvider {
|
|
143
|
+
forRuntime(runtimeId: string): SyncEndpointSecurityCapabilities;
|
|
144
|
+
/**
|
|
145
|
+
* Must stop admission synchronously, then drain and destroy this runtime's
|
|
146
|
+
* keys.
|
|
147
|
+
*/
|
|
148
|
+
revokeRuntime(runtimeId: string): void | Promise<void>;
|
|
149
|
+
}
|
|
136
150
|
export interface SyncHostServiceOptions {
|
|
151
|
+
localProvider?: (pluginId: string) => SyncHostLocalProvider | undefined;
|
|
152
|
+
/** Required before local mutations or receipt recovery can enter a provider. */
|
|
153
|
+
guardLocalApply?: SyncLocalApplyGuard;
|
|
154
|
+
/**
|
|
155
|
+
* Select custody per plugin; returning undefined explicitly retains legacy
|
|
156
|
+
* custody.
|
|
157
|
+
*/
|
|
158
|
+
securityProvider?: (pluginId: string) => SyncHostSecurityProvider | undefined;
|
|
159
|
+
/** Dedicated host custody; omitted only for legacy AppDatabase-backed callers. */
|
|
160
|
+
securityStore?: SyncSecurityStore;
|
|
137
161
|
flushOpenEditor?: (path?: string) => Promise<{
|
|
138
162
|
flushed: boolean;
|
|
139
163
|
path?: string;
|
|
@@ -155,12 +179,17 @@ export declare class SyncHostService {
|
|
|
155
179
|
readonly database: AppDatabase;
|
|
156
180
|
readonly vault: Vault;
|
|
157
181
|
readonly clock: () => number;
|
|
158
|
-
readonly options: SyncHostServiceOptions
|
|
182
|
+
readonly options: Omit<SyncHostServiceOptions, "securityStore" | "securityProvider" | "localProvider" | "guardLocalApply">;
|
|
159
183
|
constructor(database: AppDatabase, vault: Vault, clock?: () => number, options?: SyncHostServiceOptions);
|
|
160
184
|
ready(): Promise<void>;
|
|
161
185
|
forPlugin(pluginId: string, grantedCapabilities?: readonly HostedPluginCapability[]): SyncHostCapabilities;
|
|
162
|
-
revokePlugin(pluginId: string): void
|
|
163
|
-
/**
|
|
186
|
+
revokePlugin(pluginId: string): Promise<void>;
|
|
187
|
+
/** Invalidate first, then wait for every admitted host cleanup and file event. */
|
|
188
|
+
dispose(): Promise<void>;
|
|
189
|
+
/**
|
|
190
|
+
* Host-controlled enrollment; this operation is intentionally absent from
|
|
191
|
+
* plugin facades.
|
|
192
|
+
*/
|
|
164
193
|
enrollPluginCrypto(pluginId: string, passphrase: string, request?: SyncCryptoEnrollmentRequest): Promise<SyncCryptoStatus>;
|
|
165
194
|
/** Host-controlled unlock; plugins can only observe locked state. */
|
|
166
195
|
unlockPluginCrypto(pluginId: string, passphrase: string): Promise<SyncCryptoStatus>;
|