@lapis-notes/api 0.1.0 → 0.1.2

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +5 -0
  3. package/dist/app-database-host.d.ts +1 -0
  4. package/dist/app-database-host.js +1 -0
  5. package/dist/browser-sync-security.d.ts +45 -0
  6. package/dist/browser-sync-security.js +189 -0
  7. package/dist/build-tools/vite.d.ts +15 -0
  8. package/dist/build-tools/vite.js +36 -0
  9. package/dist/cache.svelte.d.ts +1 -1
  10. package/dist/cache.svelte.js +4 -1
  11. package/dist/components/editor/editor.css +54 -2
  12. package/dist/components/editor/extensions/lint/lapis-lint-tooltip.svelte +18 -5
  13. package/dist/components/editor/extensions/lint/lint-tooltip-actions.d.ts +0 -2
  14. package/dist/components/editor/extensions/lint/lint-tooltip-actions.js +0 -14
  15. package/dist/components/editor/language-service/index.js +23 -2
  16. package/dist/components/icon/icon-list.svelte.d.ts +1 -1
  17. package/dist/components/select/select.svelte.d.ts +1 -1
  18. package/dist/context.svelte.d.ts +11 -1
  19. package/dist/context.svelte.js +38 -1
  20. package/dist/docs-api.d.ts +2 -0
  21. package/dist/docs-api.js +1 -0
  22. package/dist/generated/plugin-host-modules.schema.generated.d.ts +443 -13
  23. package/dist/generated/plugin-host-modules.schema.generated.js +970 -306
  24. package/dist/index.d.ts +5 -0
  25. package/dist/index.js +5 -0
  26. package/dist/language-service/manager.d.ts +5 -0
  27. package/dist/language-service/manager.js +58 -14
  28. package/dist/lapis-extension.d.ts +1 -1
  29. package/dist/lapis-extension.js +12 -0
  30. package/dist/nostr-signer.d.ts +68 -0
  31. package/dist/nostr-signer.js +49 -0
  32. package/dist/plugin-asset-server.d.ts +1 -0
  33. package/dist/plugin-asset-server.js +3 -0
  34. package/dist/plugin-capability-facade.d.ts +1 -1
  35. package/dist/plugin-capability-facade.js +6 -0
  36. package/dist/plugin-distribution/catalog-media.d.ts +4 -0
  37. package/dist/plugin-distribution/catalog-media.js +40 -0
  38. package/dist/plugin-distribution/download-stats.d.ts +4 -0
  39. package/dist/plugin-distribution/download-stats.js +19 -0
  40. package/dist/plugin-distribution/index.d.ts +4 -0
  41. package/dist/plugin-distribution/index.js +4 -0
  42. package/dist/plugin-distribution/installer.d.ts +3 -0
  43. package/dist/plugin-distribution/installer.js +9 -2
  44. package/dist/plugin-distribution/manager.d.ts +71 -5
  45. package/dist/plugin-distribution/manager.js +376 -37
  46. package/dist/plugin-distribution/nostr-registry-adapter.d.ts +29 -0
  47. package/dist/plugin-distribution/nostr-registry-adapter.js +152 -0
  48. package/dist/plugin-distribution/plugin-bundle-verifier.d.ts +43 -0
  49. package/dist/plugin-distribution/plugin-bundle-verifier.js +64 -1
  50. package/dist/plugin-distribution/plugin-bundle.d.ts +14 -0
  51. package/dist/plugin-distribution/plugin-bundle.js +46 -7
  52. package/dist/plugin-distribution/plugin-markdown.d.ts +6 -0
  53. package/dist/plugin-distribution/plugin-markdown.js +94 -0
  54. package/dist/plugin-distribution/registry-client.d.ts +6 -1
  55. package/dist/plugin-distribution/registry-client.js +32 -10
  56. package/dist/plugin-distribution/registry-source-state.d.ts +15 -0
  57. package/dist/plugin-distribution/registry-source-state.js +42 -0
  58. package/dist/plugin-distribution/schemas.d.ts +2511 -2824
  59. package/dist/plugin-distribution/schemas.js +111 -42
  60. package/dist/plugin-distribution/types.d.ts +116 -5
  61. package/dist/plugin-distribution-host.d.ts +4 -0
  62. package/dist/plugin-distribution-host.js +2 -0
  63. package/dist/plugin-install-progress.js +1 -1
  64. package/dist/plugin-manager.d.ts +3 -0
  65. package/dist/plugin-manager.js +21 -13
  66. package/dist/plugin-settings.d.ts +21 -0
  67. package/dist/plugin-settings.js +41 -0
  68. package/dist/plugin.d.ts +13 -0
  69. package/dist/plugin.js +56 -0
  70. package/dist/registry-schema/catalog-v1.d.ts +4143 -0
  71. package/dist/registry-schema/catalog-v1.js +172 -0
  72. package/dist/registry-schema/catalog-v2.d.ts +5275 -0
  73. package/dist/registry-schema/catalog-v2.js +137 -0
  74. package/dist/registry-schema/index.d.ts +1 -0
  75. package/dist/registry-schema/index.js +1 -0
  76. package/dist/registry-schema/release-notes.d.ts +5 -0
  77. package/dist/registry-schema/release-notes.js +21 -0
  78. package/dist/server-host.d.ts +28 -0
  79. package/dist/server-host.js +17 -0
  80. package/dist/storage/app-database.d.ts +37 -2
  81. package/dist/storage/app-database.js +41 -1
  82. package/dist/storage/browser-coordinated-app-database.d.ts +3 -1
  83. package/dist/storage/browser-coordinated-app-database.js +8 -0
  84. package/dist/storage/browser.d.ts +4 -1
  85. package/dist/storage/browser.js +84 -0
  86. package/dist/storage/desktop-native.d.ts +4 -2
  87. package/dist/storage/desktop-native.js +90 -1
  88. package/dist/storage/fs.d.ts +17 -0
  89. package/dist/storage/index.d.ts +1 -0
  90. package/dist/storage/index.js +1 -0
  91. package/dist/storage/memory-vault-adapter.d.ts +3 -1
  92. package/dist/storage/memory-vault-adapter.js +60 -0
  93. package/dist/storage/remote-app-database.d.ts +19 -0
  94. package/dist/storage/remote-app-database.js +130 -0
  95. package/dist/storage/task-projection.d.ts +2 -1
  96. package/dist/storage/task-projection.js +7 -2
  97. package/dist/storage/turso-app-database.d.ts +3 -1
  98. package/dist/storage/turso-app-database.js +45 -0
  99. package/dist/storage/vault-session.js +11 -0
  100. package/dist/storage/vault-state.d.ts +1 -1
  101. package/dist/storage/vault.svelte.d.ts +3 -1
  102. package/dist/storage/vault.svelte.js +76 -3
  103. package/dist/sync-host-security.d.ts +285 -0
  104. package/dist/sync-host-security.js +2237 -0
  105. package/dist/sync-host.d.ts +179 -0
  106. package/dist/sync-host.js +769 -0
  107. package/dist/workspace.svelte.d.ts +1 -1
  108. package/dist/workspace.svelte.js +1 -1
  109. 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/v1/index.json",
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
- registryClient;
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.registryClient = new PluginRegistryClient({
51
- trustedKeys: this.trustedKeys,
52
- fetch: options.fetch,
53
- cache: new PluginRegistryCache({ adapter: options.adapter }),
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
- const source = this.registries.find((registry) => registry.enabled);
68
- if (!source) {
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 refreshed = await this.registryClient.refresh(source);
77
- this.catalog = refreshed.index;
78
- this.revocations = refreshed.revocations;
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
- return refreshed.index;
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.getCatalogEntry(pluginId);
249
+ async getPluginDetail(pluginId, options = {}) {
250
+ const entry = this.entryForSource(pluginId, options.registryId);
115
251
  if (!entry)
116
252
  return null;
117
- const result = await this.registryClient.getDetail(entry);
118
- return result.detail;
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.requireEntry(pluginId);
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: options.registryId ?? this.sourceForEntry(entry)?.id,
151
- registryUrl: this.sourceForEntry(entry)?.url,
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
- const record = await this.installer.install(installOptions);
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 installOptions = {
174
- bundle,
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: ({ phase, message, ...details }) => {
180
- this.emitProgress(progressPluginId, phase, message, details);
181
- },
356
+ onProgress,
182
357
  };
183
- const record = await this.installer.installBundle(installOptions);
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 entry = this.getCatalogEntry(pluginId);
197
- if (!installed || !entry)
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 entry = this.getCatalogEntry(record.pluginId);
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 (this.registries.find((source) => source.enabled &&
303
- (entry.channel === "official") === (source.trustTier === "official")) ?? null);
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 match = this.revocations?.revoked.find((revocation) => revocation.pluginId === record.pluginId &&
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;