@lapis-notes/api 0.1.0 → 0.1.3
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 +39 -0
- package/README.md +5 -0
- package/dist/app-database-host.d.ts +1 -0
- package/dist/app-database-host.js +1 -0
- package/dist/browser-sync-security.d.ts +45 -0
- package/dist/browser-sync-security.js +189 -0
- package/dist/build-tools/vite.d.ts +15 -0
- package/dist/build-tools/vite.js +36 -0
- package/dist/cache.svelte.d.ts +1 -1
- package/dist/cache.svelte.js +4 -1
- package/dist/components/editor/editor.css +54 -2
- package/dist/components/editor/extensions/lint/lapis-lint-tooltip.svelte +18 -5
- package/dist/components/editor/extensions/lint/lint-tooltip-actions.d.ts +0 -2
- package/dist/components/editor/extensions/lint/lint-tooltip-actions.js +0 -14
- package/dist/components/editor/language-service/index.js +23 -2
- package/dist/components/icon/icon-list.svelte.d.ts +1 -1
- package/dist/components/select/select.svelte.d.ts +1 -1
- package/dist/context.svelte.d.ts +11 -1
- package/dist/context.svelte.js +38 -1
- package/dist/docs-api.d.ts +2 -0
- package/dist/docs-api.js +1 -0
- package/dist/generated/plugin-host-modules.schema.generated.d.ts +443 -13
- package/dist/generated/plugin-host-modules.schema.generated.js +970 -306
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/language-service/manager.d.ts +5 -0
- package/dist/language-service/manager.js +58 -14
- package/dist/lapis-extension.d.ts +1 -1
- package/dist/lapis-extension.js +12 -0
- package/dist/nostr-signer.d.ts +68 -0
- package/dist/nostr-signer.js +49 -0
- package/dist/plugin-asset-server.d.ts +1 -0
- package/dist/plugin-asset-server.js +3 -0
- package/dist/plugin-capability-facade.d.ts +1 -1
- package/dist/plugin-capability-facade.js +6 -0
- package/dist/plugin-distribution/catalog-media.d.ts +4 -0
- package/dist/plugin-distribution/catalog-media.js +40 -0
- package/dist/plugin-distribution/download-stats.d.ts +4 -0
- package/dist/plugin-distribution/download-stats.js +19 -0
- package/dist/plugin-distribution/index.d.ts +4 -0
- package/dist/plugin-distribution/index.js +4 -0
- package/dist/plugin-distribution/installer.d.ts +3 -0
- package/dist/plugin-distribution/installer.js +26 -3
- package/dist/plugin-distribution/manager.d.ts +71 -5
- package/dist/plugin-distribution/manager.js +376 -37
- package/dist/plugin-distribution/nostr-registry-adapter.d.ts +29 -0
- package/dist/plugin-distribution/nostr-registry-adapter.js +152 -0
- package/dist/plugin-distribution/plugin-bundle-verifier.d.ts +43 -0
- package/dist/plugin-distribution/plugin-bundle-verifier.js +64 -1
- package/dist/plugin-distribution/plugin-bundle.d.ts +14 -0
- package/dist/plugin-distribution/plugin-bundle.js +46 -7
- package/dist/plugin-distribution/plugin-markdown.d.ts +6 -0
- package/dist/plugin-distribution/plugin-markdown.js +94 -0
- package/dist/plugin-distribution/registry-client.d.ts +6 -1
- package/dist/plugin-distribution/registry-client.js +32 -10
- package/dist/plugin-distribution/registry-source-state.d.ts +15 -0
- package/dist/plugin-distribution/registry-source-state.js +42 -0
- package/dist/plugin-distribution/schemas.d.ts +2511 -2824
- package/dist/plugin-distribution/schemas.js +111 -42
- package/dist/plugin-distribution/types.d.ts +116 -5
- package/dist/plugin-distribution-host.d.ts +4 -0
- package/dist/plugin-distribution-host.js +2 -0
- package/dist/plugin-install-progress.js +1 -1
- package/dist/plugin-manager.d.ts +3 -0
- package/dist/plugin-manager.js +21 -13
- package/dist/plugin-settings.d.ts +21 -0
- package/dist/plugin-settings.js +41 -0
- package/dist/plugin.d.ts +13 -0
- package/dist/plugin.js +56 -0
- package/dist/registry-schema/catalog-v1.d.ts +4143 -0
- package/dist/registry-schema/catalog-v1.js +172 -0
- package/dist/registry-schema/catalog-v2.d.ts +5275 -0
- package/dist/registry-schema/catalog-v2.js +137 -0
- package/dist/registry-schema/index.d.ts +1 -0
- package/dist/registry-schema/index.js +1 -0
- package/dist/registry-schema/release-notes.d.ts +5 -0
- package/dist/registry-schema/release-notes.js +21 -0
- package/dist/server-host.d.ts +28 -0
- package/dist/server-host.js +17 -0
- package/dist/storage/app-database.d.ts +37 -2
- package/dist/storage/app-database.js +41 -1
- package/dist/storage/browser-coordinated-app-database.d.ts +3 -1
- package/dist/storage/browser-coordinated-app-database.js +8 -0
- package/dist/storage/browser.d.ts +4 -1
- package/dist/storage/browser.js +84 -0
- package/dist/storage/desktop-native.d.ts +4 -2
- package/dist/storage/desktop-native.js +90 -1
- package/dist/storage/fs.d.ts +17 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/memory-vault-adapter.d.ts +3 -1
- package/dist/storage/memory-vault-adapter.js +60 -0
- package/dist/storage/remote-app-database.d.ts +19 -0
- package/dist/storage/remote-app-database.js +130 -0
- package/dist/storage/task-projection.d.ts +2 -1
- package/dist/storage/task-projection.js +7 -2
- package/dist/storage/turso-app-database.d.ts +3 -1
- package/dist/storage/turso-app-database.js +45 -0
- package/dist/storage/vault-session.js +11 -0
- package/dist/storage/vault-state.d.ts +1 -1
- package/dist/storage/vault.svelte.d.ts +3 -1
- package/dist/storage/vault.svelte.js +76 -3
- package/dist/sync-host-security.d.ts +285 -0
- package/dist/sync-host-security.js +2237 -0
- package/dist/sync-host.d.ts +179 -0
- package/dist/sync-host.js +769 -0
- package/dist/workspace.svelte.d.ts +1 -1
- package/dist/workspace.svelte.js +1 -1
- package/package.json +48 -10
|
@@ -4,12 +4,15 @@ import { checkCatalogEntryCompatibility, checkCatalogReleaseCompatibility, compa
|
|
|
4
4
|
import { PluginDistributionError } from "./errors";
|
|
5
5
|
import { InstalledPluginStateStore } from "./installed-plugin-state";
|
|
6
6
|
import { VerifiedPluginInstaller, } from "./installer";
|
|
7
|
+
import { parseNostrPluginBundle, } from "./plugin-bundle";
|
|
7
8
|
import { PluginRegistryCache } from "./registry-cache";
|
|
8
9
|
import { PluginRegistryClient } from "./registry-client";
|
|
10
|
+
import { PluginRegistrySourcePreferenceStore } from "./registry-source-state";
|
|
9
11
|
export const DEFAULT_OFFICIAL_PLUGIN_REGISTRY_SOURCE = {
|
|
10
12
|
id: "lapis-official",
|
|
11
13
|
name: "Lapis Official Plugins",
|
|
12
|
-
url: "https://registry.lapis.md/
|
|
14
|
+
url: "https://registry.lapis.md/v2/index.json",
|
|
15
|
+
downloadStatsUrl: "https://registry.lapis.md/stats/summary.json",
|
|
13
16
|
trustTier: "official",
|
|
14
17
|
enabled: true,
|
|
15
18
|
builtin: true,
|
|
@@ -34,24 +37,47 @@ export const EMBEDDED_PLUGIN_REGISTRY_KEYS = [
|
|
|
34
37
|
export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
35
38
|
options;
|
|
36
39
|
catalog = null;
|
|
37
|
-
|
|
40
|
+
httpClients = new Map();
|
|
41
|
+
sourceCatalogs = new Map();
|
|
38
42
|
stateStore;
|
|
43
|
+
sourcePreferenceStore;
|
|
39
44
|
installer;
|
|
40
45
|
registries;
|
|
41
46
|
trustedKeys;
|
|
47
|
+
registryAdapters;
|
|
48
|
+
sourceStates = new Map();
|
|
49
|
+
entrySources = new Map();
|
|
42
50
|
installedIds = new Set();
|
|
43
51
|
revocations = null;
|
|
52
|
+
sourcePreferencesLoaded = null;
|
|
44
53
|
constructor(options) {
|
|
45
54
|
super();
|
|
46
55
|
this.options = options;
|
|
47
|
-
this.registries = options.registries ?? DEFAULT_PLUGIN_REGISTRY_SOURCES;
|
|
56
|
+
this.registries = (options.registries ?? DEFAULT_PLUGIN_REGISTRY_SOURCES).map((source) => ({ ...source }));
|
|
48
57
|
this.trustedKeys = options.trustedKeys ?? EMBEDDED_PLUGIN_REGISTRY_KEYS;
|
|
49
58
|
this.stateStore = new InstalledPluginStateStore(options.adapter);
|
|
50
|
-
this.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
this.sourcePreferenceStore = new PluginRegistrySourcePreferenceStore(options.adapter);
|
|
60
|
+
const httpAdapter = {
|
|
61
|
+
refresh: async (source) => {
|
|
62
|
+
const result = await this.httpClient(source).refresh(source);
|
|
63
|
+
return {
|
|
64
|
+
index: result.index,
|
|
65
|
+
revocations: result.revocations,
|
|
66
|
+
installReady: true,
|
|
67
|
+
stale: result.stale,
|
|
68
|
+
...(result.stale
|
|
69
|
+
? { message: "Using cached registry metadata" }
|
|
70
|
+
: {}),
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
getDetail: async (source, entry) => (await this.httpClient(source).getDetail(entry)).detail,
|
|
74
|
+
getDownloadStats: (source, refreshOptions) => this.httpClient(source).getDownloadStats(source, refreshOptions),
|
|
75
|
+
};
|
|
76
|
+
this.registryAdapters = new Map([
|
|
77
|
+
["http", httpAdapter],
|
|
78
|
+
...Object.entries(options.registryAdapters ?? {}).filter((entry) => entry[1] !== undefined),
|
|
79
|
+
]);
|
|
80
|
+
this.syncSourceStates();
|
|
55
81
|
this.installer = new VerifiedPluginInstaller({
|
|
56
82
|
adapter: options.adapter,
|
|
57
83
|
trustedKeys: this.trustedKeys,
|
|
@@ -63,21 +89,131 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
63
89
|
});
|
|
64
90
|
}
|
|
65
91
|
async refreshCatalog(_options = {}) {
|
|
92
|
+
await this.ensureSourcePreferencesLoaded();
|
|
66
93
|
this.emitProgress("*", "fetching-catalog");
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
this.entrySources.clear();
|
|
95
|
+
this.syncSourceStates();
|
|
96
|
+
const sources = this.registries.filter((registry) => registry.enabled);
|
|
97
|
+
if (!sources.length) {
|
|
69
98
|
this.catalog = {
|
|
70
99
|
schemaVersion: 1,
|
|
71
100
|
generatedAt: new Date(0).toISOString(),
|
|
72
101
|
plugins: [],
|
|
73
102
|
};
|
|
103
|
+
this.revocations = null;
|
|
74
104
|
return this.catalog;
|
|
75
105
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
const indexes = [];
|
|
107
|
+
const errors = [];
|
|
108
|
+
for (const source of sources) {
|
|
109
|
+
const adapter = this.adapterForSource(source);
|
|
110
|
+
if (!adapter) {
|
|
111
|
+
const message = `No ${this.sourceKind(source)} registry adapter is available`;
|
|
112
|
+
this.sourceStates.set(source.id, {
|
|
113
|
+
source,
|
|
114
|
+
phase: "error",
|
|
115
|
+
installReady: false,
|
|
116
|
+
message,
|
|
117
|
+
});
|
|
118
|
+
errors.push(new Error(message));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const result = await adapter.refresh(source, _options);
|
|
123
|
+
indexes.push({ source, result });
|
|
124
|
+
this.sourceCatalogs.set(source.id, result);
|
|
125
|
+
this.sourceStates.set(source.id, {
|
|
126
|
+
source,
|
|
127
|
+
phase: result.stale ? "degraded" : "ready",
|
|
128
|
+
installReady: result.installReady,
|
|
129
|
+
...(result.message ? { message: result.message } : {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
errors.push(error);
|
|
134
|
+
this.sourceStates.set(source.id, {
|
|
135
|
+
source,
|
|
136
|
+
phase: "error",
|
|
137
|
+
installReady: false,
|
|
138
|
+
message: error instanceof Error ? error.message : String(error),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!indexes.length)
|
|
143
|
+
throw errors[0] ?? new Error("No registry source is available");
|
|
144
|
+
const plugins = [];
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
const registries = {};
|
|
147
|
+
for (const { source, result } of [...indexes].sort((a, b) => Number(this.sourceKind(a.source) === "nostr") -
|
|
148
|
+
Number(this.sourceKind(b.source) === "nostr"))) {
|
|
149
|
+
registries[source.id] = {
|
|
150
|
+
name: source.name,
|
|
151
|
+
trustTier: source.trustTier,
|
|
152
|
+
};
|
|
153
|
+
for (const entry of result.index.plugins) {
|
|
154
|
+
if (seen.has(entry.id))
|
|
155
|
+
continue;
|
|
156
|
+
seen.add(entry.id);
|
|
157
|
+
plugins.push(entry);
|
|
158
|
+
this.entrySources.set(entry.id, source);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.catalog = {
|
|
162
|
+
schemaVersion: 2,
|
|
163
|
+
generatedAt: indexes
|
|
164
|
+
.map(({ result }) => result.index.generatedAt)
|
|
165
|
+
.sort()
|
|
166
|
+
.at(-1),
|
|
167
|
+
registries,
|
|
168
|
+
plugins,
|
|
169
|
+
};
|
|
170
|
+
this.revocations = {
|
|
171
|
+
schemaVersion: 1,
|
|
172
|
+
generatedAt: this.catalog.generatedAt,
|
|
173
|
+
revoked: indexes.flatMap(({ result }) => result.revocations?.revoked ?? []),
|
|
174
|
+
};
|
|
79
175
|
await this.applyRevocationsToInstalled();
|
|
80
|
-
|
|
176
|
+
await this.revalidateInstalledNostrProofs();
|
|
177
|
+
return this.catalog;
|
|
178
|
+
}
|
|
179
|
+
async getDownloadStats(options = {}) {
|
|
180
|
+
await this.ensureSourcePreferencesLoaded();
|
|
181
|
+
for (const source of this.registries.filter((entry) => entry.enabled)) {
|
|
182
|
+
const adapter = this.adapterForSource(source);
|
|
183
|
+
if (!adapter?.getDownloadStats)
|
|
184
|
+
continue;
|
|
185
|
+
const result = await adapter.getDownloadStats(source, options);
|
|
186
|
+
if (result)
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
listSources() {
|
|
192
|
+
return this.registries.map((source) => this.sourceStates.get(source.id) ?? {
|
|
193
|
+
source,
|
|
194
|
+
phase: "idle",
|
|
195
|
+
installReady: false,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async setSourceEnabled(sourceId, enabled) {
|
|
199
|
+
await this.ensureSourcePreferencesLoaded();
|
|
200
|
+
const target = this.registries.find((source) => source.id === sourceId);
|
|
201
|
+
if (!target) {
|
|
202
|
+
throw new PluginDistributionError("metadata-invalid", `Registry source ${sourceId} is not configured.`);
|
|
203
|
+
}
|
|
204
|
+
this.registries = this.registries.map((source) => source.id === sourceId ? { ...source, enabled } : source);
|
|
205
|
+
this.catalog = null;
|
|
206
|
+
this.revocations = null;
|
|
207
|
+
this.entrySources.clear();
|
|
208
|
+
this.syncSourceStates();
|
|
209
|
+
await this.sourcePreferenceStore.save({
|
|
210
|
+
schemaVersion: 1,
|
|
211
|
+
updatedAt: new Date().toISOString(),
|
|
212
|
+
sources: Object.fromEntries(this.registries.map((source) => [
|
|
213
|
+
source.id,
|
|
214
|
+
{ enabled: source.enabled },
|
|
215
|
+
])),
|
|
216
|
+
});
|
|
81
217
|
}
|
|
82
218
|
search(query = {}) {
|
|
83
219
|
const text = query.text?.trim().toLowerCase();
|
|
@@ -110,17 +246,26 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
110
246
|
getCatalogEntry(pluginId) {
|
|
111
247
|
return this.catalog?.plugins.find((entry) => entry.id === pluginId);
|
|
112
248
|
}
|
|
113
|
-
async getPluginDetail(pluginId) {
|
|
114
|
-
const entry = this.
|
|
249
|
+
async getPluginDetail(pluginId, options = {}) {
|
|
250
|
+
const entry = this.entryForSource(pluginId, options.registryId);
|
|
115
251
|
if (!entry)
|
|
116
252
|
return null;
|
|
117
|
-
const
|
|
118
|
-
|
|
253
|
+
const source = options.registryId
|
|
254
|
+
? this.registries.find((candidate) => candidate.id === options.registryId)
|
|
255
|
+
: this.sourceForEntry(entry);
|
|
256
|
+
const adapter = source ? this.adapterForSource(source) : undefined;
|
|
257
|
+
if (!source || !adapter)
|
|
258
|
+
return null;
|
|
259
|
+
return adapter.getDetail(source, entry);
|
|
119
260
|
}
|
|
120
261
|
async install(pluginId, options = {}) {
|
|
121
262
|
try {
|
|
122
263
|
this.emitProgress(pluginId, "resolving", "Resolving plugin");
|
|
123
|
-
const entry = this.
|
|
264
|
+
const entry = this.entryForSource(pluginId, options.registryId);
|
|
265
|
+
if (!entry)
|
|
266
|
+
throw new PluginDistributionError("metadata-invalid", `Plugin ${pluginId} is unavailable from the requested source.`);
|
|
267
|
+
if (["pending", "disabled", "revoked"].includes(entry.status ?? "active"))
|
|
268
|
+
throw new PluginDistributionError("metadata-invalid", `Plugin ${pluginId} is ${entry.status}.`);
|
|
124
269
|
if (options.requireOfficial && entry.channel !== "official") {
|
|
125
270
|
throw new PluginDistributionError("metadata-invalid", `Plugin ${pluginId} is not an official registry entry.`);
|
|
126
271
|
}
|
|
@@ -129,7 +274,9 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
129
274
|
throw new PluginDistributionError("compatibility-failed", `Plugin ${pluginId} is not compatible: ${entryCompatibility.reasons.join(", ")}`, { details: { reasons: entryCompatibility.reasons } });
|
|
130
275
|
}
|
|
131
276
|
this.emitProgress(pluginId, "fetching-release", "Fetching release metadata");
|
|
132
|
-
const detail = await this.getPluginDetail(pluginId
|
|
277
|
+
const detail = await this.getPluginDetail(pluginId, {
|
|
278
|
+
registryId: options.registryId,
|
|
279
|
+
});
|
|
133
280
|
if (!detail) {
|
|
134
281
|
throw new PluginDistributionError("metadata-invalid", `Plugin ${pluginId} is missing registry detail metadata.`);
|
|
135
282
|
}
|
|
@@ -142,13 +289,17 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
142
289
|
throw new PluginDistributionError("compatibility-failed", `Plugin ${pluginId} release is not compatible: ${releaseCompatibility.reasons.join(", ")}`, { details: { reasons: releaseCompatibility.reasons } });
|
|
143
290
|
}
|
|
144
291
|
this.emitProgress(pluginId, "verifying-signature", "Verifying release signature");
|
|
292
|
+
const source = options.registryId
|
|
293
|
+
? this.registries.find((candidate) => candidate.id === options.registryId)
|
|
294
|
+
: this.sourceForEntry(entry);
|
|
295
|
+
const adapter = source ? this.adapterForSource(source) : undefined;
|
|
145
296
|
const installOptions = {
|
|
146
297
|
entry,
|
|
147
298
|
detail,
|
|
148
299
|
version: options.version,
|
|
149
300
|
enable: options.enable,
|
|
150
|
-
registryId:
|
|
151
|
-
registryUrl:
|
|
301
|
+
registryId: source?.id,
|
|
302
|
+
registryUrl: source?.url,
|
|
152
303
|
provenance: entry.channel,
|
|
153
304
|
compatibility: this.compatibilityInput(await this.isWorkspaceTrusted()),
|
|
154
305
|
signal: options.signal,
|
|
@@ -156,7 +307,32 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
156
307
|
this.emitProgress(pluginId, phase, message, details);
|
|
157
308
|
},
|
|
158
309
|
};
|
|
159
|
-
|
|
310
|
+
let record;
|
|
311
|
+
if (source && adapter?.getVerifiedBundle) {
|
|
312
|
+
const sourceState = this.sourceStates.get(source.id);
|
|
313
|
+
if (!sourceState?.installReady) {
|
|
314
|
+
throw new PluginDistributionError("metadata-invalid", `Registry source ${source.name} has not reached install quorum.`);
|
|
315
|
+
}
|
|
316
|
+
const version = options.version ?? detail.latestVersion;
|
|
317
|
+
const payload = await adapter.getVerifiedBundle(source, entry, detail, version, { signal: options.signal });
|
|
318
|
+
if (payload.releaseManifest.pluginId !== entry.id ||
|
|
319
|
+
payload.releaseManifest.version !== version ||
|
|
320
|
+
payload.releaseManifest.channel !== entry.channel) {
|
|
321
|
+
throw new PluginDistributionError("metadata-invalid", "Verified registry payload does not match catalog metadata.");
|
|
322
|
+
}
|
|
323
|
+
record = await this.installer.installVerifiedPayload(payload, {
|
|
324
|
+
enable: installOptions.enable,
|
|
325
|
+
registryId: installOptions.registryId,
|
|
326
|
+
registryUrl: installOptions.registryUrl,
|
|
327
|
+
provenance: installOptions.provenance,
|
|
328
|
+
compatibility: installOptions.compatibility,
|
|
329
|
+
signal: installOptions.signal,
|
|
330
|
+
onProgress: installOptions.onProgress,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
record = await this.installer.install(installOptions);
|
|
335
|
+
}
|
|
160
336
|
this.installedIds.add(pluginId);
|
|
161
337
|
this.emitProgress(pluginId, "complete", "Plugin installed");
|
|
162
338
|
return record;
|
|
@@ -170,17 +346,35 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
170
346
|
let progressPluginId = "*";
|
|
171
347
|
try {
|
|
172
348
|
this.emitProgress(progressPluginId, "verifying-signature", "Verifying plugin bundle");
|
|
173
|
-
const
|
|
174
|
-
|
|
349
|
+
const onProgress = ({ phase, message, ...details }) => {
|
|
350
|
+
this.emitProgress(progressPluginId, phase, message, details);
|
|
351
|
+
};
|
|
352
|
+
const commonOptions = {
|
|
175
353
|
enable: options.enable,
|
|
176
|
-
provenance: "official",
|
|
177
354
|
compatibility: this.compatibilityInput(await this.isWorkspaceTrusted()),
|
|
178
355
|
signal: options.signal,
|
|
179
|
-
onProgress
|
|
180
|
-
this.emitProgress(progressPluginId, phase, message, details);
|
|
181
|
-
},
|
|
356
|
+
onProgress,
|
|
182
357
|
};
|
|
183
|
-
const
|
|
358
|
+
const portable = parseNostrPluginBundle(bundle, {
|
|
359
|
+
onProgress: commonOptions.onProgress,
|
|
360
|
+
});
|
|
361
|
+
let record;
|
|
362
|
+
if (portable) {
|
|
363
|
+
const verified = await this.verifyOfflineNostrBundle(portable, options.signal);
|
|
364
|
+
record = await this.installer.installVerifiedPayload(verified.payload, {
|
|
365
|
+
...commonOptions,
|
|
366
|
+
provenance: verified.payload.releaseManifest.channel,
|
|
367
|
+
registryId: verified.source.id,
|
|
368
|
+
registryUrl: verified.source.url,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
record = await this.installer.installBundle({
|
|
373
|
+
bundle,
|
|
374
|
+
...commonOptions,
|
|
375
|
+
provenance: "official",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
184
378
|
progressPluginId = record.pluginId;
|
|
185
379
|
this.installedIds.add(record.pluginId);
|
|
186
380
|
this.emitProgress(record.pluginId, "complete", "Plugin installed");
|
|
@@ -191,10 +385,38 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
191
385
|
throw error;
|
|
192
386
|
}
|
|
193
387
|
}
|
|
388
|
+
async verifyOfflineNostrBundle(bundle, signal) {
|
|
389
|
+
await this.ensureSourcePreferencesLoaded();
|
|
390
|
+
const failures = [];
|
|
391
|
+
for (const source of this.registries.filter((candidate) => candidate.enabled && this.sourceKind(candidate) === "nostr")) {
|
|
392
|
+
const adapter = this.adapterForSource(source);
|
|
393
|
+
if (!adapter?.verifyOfflineBundle)
|
|
394
|
+
continue;
|
|
395
|
+
try {
|
|
396
|
+
return {
|
|
397
|
+
payload: await adapter.verifyOfflineBundle(source, bundle, {
|
|
398
|
+
signal,
|
|
399
|
+
}),
|
|
400
|
+
source,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
failures.push(error);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (failures.length)
|
|
408
|
+
throw failures[0];
|
|
409
|
+
throw new PluginDistributionError("metadata-invalid", "No enabled Nostr source can verify this offline plugin bundle.");
|
|
410
|
+
}
|
|
194
411
|
async update(pluginId, version, options = {}) {
|
|
195
412
|
const installed = await this.getInstalled(pluginId);
|
|
196
|
-
const
|
|
197
|
-
|
|
413
|
+
const pinnedSource = installed
|
|
414
|
+
? this.installedSource(installed)
|
|
415
|
+
: undefined;
|
|
416
|
+
const entry = pinnedSource
|
|
417
|
+
? this.entryForSource(pluginId, pinnedSource.id)
|
|
418
|
+
: undefined;
|
|
419
|
+
if (!installed || !entry || !pinnedSource)
|
|
198
420
|
return null;
|
|
199
421
|
const targetVersion = version ?? installed.revoked?.replacementVersion ?? entry.latestVersion;
|
|
200
422
|
if (compareVersions(targetVersion, installed.installedVersion) <= 0) {
|
|
@@ -203,6 +425,7 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
203
425
|
const wasEnabled = Boolean(this.options.pluginManager?.isPluginEnabled?.(pluginId));
|
|
204
426
|
const updated = await this.install(pluginId, {
|
|
205
427
|
version: targetVersion,
|
|
428
|
+
registryId: pinnedSource.id,
|
|
206
429
|
enable: wasEnabled,
|
|
207
430
|
signal: options.signal,
|
|
208
431
|
});
|
|
@@ -231,7 +454,10 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
231
454
|
for (const record of installed) {
|
|
232
455
|
if (record.provenance !== "official")
|
|
233
456
|
continue;
|
|
234
|
-
const
|
|
457
|
+
const pinnedSource = this.installedSource(record);
|
|
458
|
+
const entry = pinnedSource
|
|
459
|
+
? this.entryForSource(record.pluginId, pinnedSource.id)
|
|
460
|
+
: undefined;
|
|
235
461
|
const revoked = this.revocationFor(record) ?? record.revoked;
|
|
236
462
|
if (!entry) {
|
|
237
463
|
if (revoked) {
|
|
@@ -257,7 +483,9 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
257
483
|
if (!hasNewerTarget && !revoked)
|
|
258
484
|
continue;
|
|
259
485
|
let compatibility = checkCatalogEntryCompatibility(entry, this.compatibilityInput(false));
|
|
260
|
-
const detail = await this.getPluginDetail(record.pluginId
|
|
486
|
+
const detail = await this.getPluginDetail(record.pluginId, {
|
|
487
|
+
registryId: pinnedSource?.id,
|
|
488
|
+
});
|
|
261
489
|
const targetRelease = detail?.versions[targetVersion];
|
|
262
490
|
if (targetRelease) {
|
|
263
491
|
compatibility = checkCatalogReleaseCompatibility(targetRelease, this.compatibilityInput(false));
|
|
@@ -291,6 +519,46 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
291
519
|
this.on("progress", listener);
|
|
292
520
|
return () => this.off("progress", listener);
|
|
293
521
|
}
|
|
522
|
+
httpClient(source) {
|
|
523
|
+
const key = `${source.id}:${source.url}`;
|
|
524
|
+
let client = this.httpClients.get(key);
|
|
525
|
+
if (!client) {
|
|
526
|
+
client = new PluginRegistryClient({
|
|
527
|
+
trustedKeys: this.trustedKeys.filter((key) => key.trustTier === source.trustTier),
|
|
528
|
+
fetch: this.options.fetch,
|
|
529
|
+
cache: new PluginRegistryCache({
|
|
530
|
+
adapter: this.options.adapter,
|
|
531
|
+
cachePath: `.obsidian/plugin-registry-cache/${encodeURIComponent(source.id)}/${encodeURIComponent(source.url)}.json`,
|
|
532
|
+
}),
|
|
533
|
+
});
|
|
534
|
+
this.httpClients.set(key, client);
|
|
535
|
+
}
|
|
536
|
+
return client;
|
|
537
|
+
}
|
|
538
|
+
entryForSource(pluginId, registryId) {
|
|
539
|
+
if (!registryId)
|
|
540
|
+
return this.getCatalogEntry(pluginId);
|
|
541
|
+
const state = this.sourceStates.get(registryId);
|
|
542
|
+
if (!state?.source.enabled || !state.installReady)
|
|
543
|
+
return undefined;
|
|
544
|
+
return this.sourceCatalogs
|
|
545
|
+
.get(registryId)
|
|
546
|
+
?.index.plugins.find((entry) => entry.id === pluginId);
|
|
547
|
+
}
|
|
548
|
+
installedSource(record) {
|
|
549
|
+
const source = record.registryId
|
|
550
|
+
? this.registries.find((candidate) => candidate.id === record.registryId)
|
|
551
|
+
: this.registries.find((candidate) => candidate.url === record.registryUrl);
|
|
552
|
+
if (!source)
|
|
553
|
+
return undefined;
|
|
554
|
+
if (!record.registryUrl || record.registryUrl === source.url)
|
|
555
|
+
return source;
|
|
556
|
+
if (source.id === "lapis-official" &&
|
|
557
|
+
source.url === "https://registry.lapis.md/v2/index.json" &&
|
|
558
|
+
record.registryUrl === "https://registry.lapis.md/v1/index.json")
|
|
559
|
+
return source;
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
294
562
|
requireEntry(pluginId) {
|
|
295
563
|
const entry = this.getCatalogEntry(pluginId);
|
|
296
564
|
if (!entry) {
|
|
@@ -299,8 +567,13 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
299
567
|
return entry;
|
|
300
568
|
}
|
|
301
569
|
sourceForEntry(entry) {
|
|
302
|
-
return
|
|
303
|
-
|
|
570
|
+
return this.entrySources.get(entry.id) ?? null;
|
|
571
|
+
}
|
|
572
|
+
sourceKind(source) {
|
|
573
|
+
return source.kind ?? "http";
|
|
574
|
+
}
|
|
575
|
+
adapterForSource(source) {
|
|
576
|
+
return this.registryAdapters.get(this.sourceKind(source));
|
|
304
577
|
}
|
|
305
578
|
compatibilityInput(workspaceTrusted) {
|
|
306
579
|
return {
|
|
@@ -316,11 +589,50 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
316
589
|
async isWorkspaceTrusted() {
|
|
317
590
|
return Boolean(await this.options.workspaceTrusted?.());
|
|
318
591
|
}
|
|
592
|
+
ensureSourcePreferencesLoaded() {
|
|
593
|
+
this.sourcePreferencesLoaded ??= this.loadSourcePreferences();
|
|
594
|
+
return this.sourcePreferencesLoaded;
|
|
595
|
+
}
|
|
596
|
+
async loadSourcePreferences() {
|
|
597
|
+
const preferences = await this.sourcePreferenceStore.load();
|
|
598
|
+
this.registries = this.registries.map((source) => {
|
|
599
|
+
const preference = preferences.sources[source.id];
|
|
600
|
+
return preference
|
|
601
|
+
? { ...source, enabled: preference.enabled }
|
|
602
|
+
: { ...source };
|
|
603
|
+
});
|
|
604
|
+
this.syncSourceStates();
|
|
605
|
+
}
|
|
606
|
+
syncSourceStates() {
|
|
607
|
+
const configuredIds = new Set(this.registries.map((source) => source.id));
|
|
608
|
+
for (const sourceId of this.sourceStates.keys()) {
|
|
609
|
+
if (!configuredIds.has(sourceId))
|
|
610
|
+
this.sourceStates.delete(sourceId);
|
|
611
|
+
}
|
|
612
|
+
for (const source of this.registries) {
|
|
613
|
+
const previous = this.sourceStates.get(source.id);
|
|
614
|
+
this.sourceStates.set(source.id, {
|
|
615
|
+
source,
|
|
616
|
+
phase: source.enabled ? (previous?.phase ?? "idle") : "idle",
|
|
617
|
+
installReady: source.enabled
|
|
618
|
+
? (previous?.installReady ?? false)
|
|
619
|
+
: false,
|
|
620
|
+
...(source.enabled && previous?.message
|
|
621
|
+
? { message: previous.message }
|
|
622
|
+
: {}),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
319
626
|
async applyRevocationsToInstalled() {
|
|
320
627
|
const installed = await this.stateStore.list();
|
|
321
628
|
for (const record of installed) {
|
|
322
629
|
if (record.provenance !== "official")
|
|
323
630
|
continue;
|
|
631
|
+
const source = this.installedSource(record);
|
|
632
|
+
if (!source ||
|
|
633
|
+
this.sourceStates.get(source.id)?.phase !== "ready" ||
|
|
634
|
+
!this.sourceCatalogs.get(source.id)?.revocations)
|
|
635
|
+
continue;
|
|
324
636
|
const revoked = this.revocationFor(record);
|
|
325
637
|
if (sameRevocation(record.revoked, revoked))
|
|
326
638
|
continue;
|
|
@@ -337,8 +649,35 @@ export class DefaultPluginDistributionManager extends EventDispatcher {
|
|
|
337
649
|
await this.stateStore.upsert(next);
|
|
338
650
|
}
|
|
339
651
|
}
|
|
652
|
+
async revalidateInstalledNostrProofs() {
|
|
653
|
+
const installed = await this.stateStore.list();
|
|
654
|
+
for (const record of installed) {
|
|
655
|
+
if (!record.nostrProof?.requiresRelayRefresh || !record.registryId) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
const state = this.sourceStates.get(record.registryId);
|
|
659
|
+
if (!state?.installReady || state.phase !== "ready")
|
|
660
|
+
continue;
|
|
661
|
+
const adapter = this.adapterForSource(state.source);
|
|
662
|
+
if (!adapter?.revalidateInstalledProof)
|
|
663
|
+
continue;
|
|
664
|
+
try {
|
|
665
|
+
const nostrProof = await adapter.revalidateInstalledProof(state.source, record);
|
|
666
|
+
await this.stateStore.upsert({
|
|
667
|
+
...record,
|
|
668
|
+
nostrProof,
|
|
669
|
+
updatedAt: new Date().toISOString(),
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
// A live source must positively revalidate the same release before the
|
|
674
|
+
// offline warning can be cleared. Failures retain the warning.
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
340
678
|
revocationFor(record) {
|
|
341
|
-
const
|
|
679
|
+
const source = this.installedSource(record);
|
|
680
|
+
const match = (source ? this.sourceCatalogs.get(source.id)?.revocations : undefined)?.revoked.find((revocation) => revocation.pluginId === record.pluginId &&
|
|
342
681
|
revocation.versions.includes(record.installedVersion));
|
|
343
682
|
if (!match)
|
|
344
683
|
return undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PluginBinaryFetch } from "./installer";
|
|
2
|
+
import type { PluginRegistryAdapter } from "./manager";
|
|
3
|
+
import { type NostrReleaseProofVerifier } from "./plugin-bundle-verifier";
|
|
4
|
+
import type { PluginCatalogEntry } from "./types";
|
|
5
|
+
export interface NostrRegistryCatalogSource {
|
|
6
|
+
loadIndex(options?: {
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}): Promise<unknown>;
|
|
9
|
+
loadPluginDetail(entry: PluginCatalogEntry, options?: {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}): Promise<unknown>;
|
|
12
|
+
loadReleaseProof(pluginId: string, version: string, options?: {
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}): Promise<unknown>;
|
|
15
|
+
getStatus(): {
|
|
16
|
+
installReady: boolean;
|
|
17
|
+
fresh: boolean;
|
|
18
|
+
message?: string;
|
|
19
|
+
};
|
|
20
|
+
refresh?(options?: {
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface CreateNostrPluginRegistryAdapterOptions {
|
|
25
|
+
catalogSource: NostrRegistryCatalogSource;
|
|
26
|
+
verifyReleaseProof: NostrReleaseProofVerifier;
|
|
27
|
+
fetch?: PluginBinaryFetch;
|
|
28
|
+
}
|
|
29
|
+
export declare function createNostrPluginRegistryAdapter(options: CreateNostrPluginRegistryAdapterOptions): PluginRegistryAdapter;
|