@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @lapis-notes/api
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Own the transport-neutral registry schema and portable Turso build helper,
8
+ removing the Community dependency from static registry producers and hosts.
9
+
10
+ - Expose host-owned secure sync capabilities and controlled declarative plugin settings.
11
+
12
+ - Allow indexed consumers to select metadata fingerprint inputs. Preserve native
13
+ filesystem error codes and reconcile files removed during startup as deletions
14
+ while retaining unexpected read failures.
15
+
16
+ - Support verified static V2 catalogs alongside Nostr sources, retain installation
17
+ source provenance, and render themed registry images and structured release notes.
18
+ - Updated dependencies []:
19
+ - @lapis-notes/ui@0.1.1
20
+
3
21
  ## 0.1.0
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -19,6 +19,11 @@ or product routing.
19
19
  - Peer dependencies for UI, Svelte, CodeMirror, Mira, and Design Core so hosts
20
20
  retain singleton control over their runtime stack.
21
21
 
22
+ `@lapis-notes/api/registry/schema` provides catalog schemas, types, normalization,
23
+ media resolution, and structured release-note extraction for registry producers
24
+ and consumers. It imports only Zod and does not boot the application.
25
+ `@lapis-notes/api/build/vite` provides the portable Turso browser-build helper.
26
+
22
27
  ## Usage
23
28
 
24
29
  ```ts
@@ -1,4 +1,5 @@
1
1
  export * from "./storage/app-database";
2
+ export * from "./storage/remote-app-database";
2
3
  export * from "./storage/index-projection";
3
4
  export * from "./storage/task-projection";
4
5
  export * from "./storage/turso-app-database";
@@ -1,4 +1,5 @@
1
1
  export * from "./storage/app-database";
2
+ export * from "./storage/remote-app-database";
2
3
  export * from "./storage/index-projection";
3
4
  export * from "./storage/task-projection";
4
5
  export * from "./storage/turso-app-database";
@@ -0,0 +1,45 @@
1
+ import type { SyncCryptoStatus } from "./sync-host-security";
2
+ import type { SyncCryptoEnrollmentRequest } from "./sync-host-security";
3
+ import type { SyncHostService } from "./sync-host";
4
+ import type { AppDatabase } from "./storage/app-database";
5
+ export interface BrowserSyncPrfCredential {
6
+ version: 1;
7
+ credentialId: string;
8
+ prfInput: string;
9
+ hkdfSalt: string;
10
+ }
11
+ export interface BrowserStoragePersistenceStatus {
12
+ supported: boolean;
13
+ persisted: boolean;
14
+ }
15
+ export interface BrowserSyncUnlockInteraction {
16
+ confirmPasskeyEnrollment(pluginId: string): Promise<boolean>;
17
+ requestPassphrase(pluginId: string, mode: "enroll" | "unlock"): Promise<string | null>;
18
+ }
19
+ export declare function supportsBrowserWebAuthnPrf(): Promise<boolean>;
20
+ export declare function requestBrowserStoragePersistence(storage?: Pick<StorageManager, "persist" | "persisted"> | undefined): Promise<BrowserStoragePersistenceStatus>;
21
+ export declare function createBrowserSyncPrfCredential(options: {
22
+ rpName: string;
23
+ rpId?: string;
24
+ userId: Uint8Array;
25
+ userName: string;
26
+ credentials?: CredentialsContainer;
27
+ }): Promise<{
28
+ credential: BrowserSyncPrfCredential;
29
+ wrappingKey: Uint8Array;
30
+ }>;
31
+ export declare function unlockBrowserSyncPrfCredential(credential: BrowserSyncPrfCredential, credentials?: CredentialsContainer | undefined): Promise<Uint8Array>;
32
+ /**
33
+ * Build the host-owned browser unlock callback used by SyncHostService.
34
+ * The callback returns only status to plugins; wrapping material stays in the
35
+ * host flow and is zeroed immediately after enrollment or unlock.
36
+ */
37
+ export declare function createBrowserSyncUnlockHandler(options: {
38
+ database: AppDatabase;
39
+ getSyncHost(): SyncHostService;
40
+ interaction: BrowserSyncUnlockInteraction;
41
+ rpName: string;
42
+ rpId?: string;
43
+ userName?: string;
44
+ credentials?: CredentialsContainer;
45
+ }): (pluginId: string, status: SyncCryptoStatus, enrollment?: SyncCryptoEnrollmentRequest) => Promise<void>;
@@ -0,0 +1,189 @@
1
+ import { hkdf } from "@noble/hashes/hkdf.js";
2
+ import { sha256 } from "@noble/hashes/sha2.js";
3
+ import { randomBytes } from "@noble/hashes/utils.js";
4
+ const BROWSER_CREDENTIAL_PREFIX = "sync-host:v1:browser-prf:";
5
+ export async function supportsBrowserWebAuthnPrf() {
6
+ if (typeof PublicKeyCredential === "undefined")
7
+ return false;
8
+ const getCapabilities = PublicKeyCredential.getClientCapabilities;
9
+ if (!getCapabilities)
10
+ return false;
11
+ try {
12
+ const capabilities = await getCapabilities();
13
+ return capabilities["extension:prf"] === true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ export async function requestBrowserStoragePersistence(storage = typeof navigator === "undefined" ? undefined : navigator.storage) {
20
+ if (!storage?.persist || !storage.persisted) {
21
+ return { supported: false, persisted: false };
22
+ }
23
+ if (await storage.persisted()) {
24
+ return { supported: true, persisted: true };
25
+ }
26
+ return { supported: true, persisted: await storage.persist() };
27
+ }
28
+ export async function createBrowserSyncPrfCredential(options) {
29
+ const credentials = options.credentials ??
30
+ (typeof navigator === "undefined" ? undefined : navigator.credentials);
31
+ if (!credentials)
32
+ throw new Error("WebAuthn is unavailable in this host");
33
+ const prfInput = randomBytes(32);
34
+ const hkdfSalt = randomBytes(32);
35
+ const created = await credentials.create({
36
+ publicKey: {
37
+ challenge: randomBytes(32),
38
+ rp: { name: options.rpName, id: options.rpId },
39
+ user: {
40
+ id: options.userId,
41
+ name: options.userName,
42
+ displayName: options.userName,
43
+ },
44
+ pubKeyCredParams: [
45
+ { type: "public-key", alg: -7 },
46
+ { type: "public-key", alg: -257 },
47
+ ],
48
+ authenticatorSelection: {
49
+ residentKey: "required",
50
+ userVerification: "required",
51
+ },
52
+ extensions: {
53
+ prf: { eval: { first: prfInput } },
54
+ },
55
+ },
56
+ });
57
+ if (!(created instanceof PublicKeyCredential)) {
58
+ throw new Error("WebAuthn credential creation was cancelled");
59
+ }
60
+ const results = created.getClientExtensionResults();
61
+ if (results.prf?.enabled !== true) {
62
+ throw new Error("The selected authenticator does not support WebAuthn PRF");
63
+ }
64
+ const credential = {
65
+ version: 1,
66
+ credentialId: toBase64Url(new Uint8Array(created.rawId)),
67
+ prfInput: toBase64Url(prfInput),
68
+ hkdfSalt: toBase64Url(hkdfSalt),
69
+ };
70
+ return {
71
+ credential,
72
+ wrappingKey: await unlockBrowserSyncPrfCredential(credential, credentials),
73
+ };
74
+ }
75
+ export async function unlockBrowserSyncPrfCredential(credential, credentials = typeof navigator === "undefined" ? undefined : navigator.credentials) {
76
+ if (!credentials)
77
+ throw new Error("WebAuthn is unavailable in this host");
78
+ if (credential.version !== 1) {
79
+ throw new Error("Unsupported browser sync credential version");
80
+ }
81
+ const assertion = await credentials.get({
82
+ publicKey: {
83
+ challenge: randomBytes(32),
84
+ allowCredentials: [
85
+ {
86
+ id: fromBase64Url(credential.credentialId),
87
+ type: "public-key",
88
+ },
89
+ ],
90
+ userVerification: "required",
91
+ extensions: {
92
+ prf: { eval: { first: fromBase64Url(credential.prfInput) } },
93
+ },
94
+ },
95
+ });
96
+ if (!(assertion instanceof PublicKeyCredential)) {
97
+ throw new Error("WebAuthn unlock was cancelled");
98
+ }
99
+ const results = assertion.getClientExtensionResults();
100
+ const output = results.prf?.results?.first;
101
+ if (!output || output.byteLength < 32) {
102
+ throw new Error("WebAuthn PRF did not return wrapping material");
103
+ }
104
+ return hkdf(sha256, new Uint8Array(output), fromBase64Url(credential.hkdfSalt), new TextEncoder().encode("lapis-sync-v1:browser-wrapping-key"), 32);
105
+ }
106
+ /**
107
+ * Build the host-owned browser unlock callback used by SyncHostService.
108
+ * The callback returns only status to plugins; wrapping material stays in the
109
+ * host flow and is zeroed immediately after enrollment or unlock.
110
+ */
111
+ export function createBrowserSyncUnlockHandler(options) {
112
+ return async (pluginId, status, enrollment) => {
113
+ if (status.state === "unlocked")
114
+ return;
115
+ const recordKey = `${BROWSER_CREDENTIAL_PREFIX}${encodeKeyPart(pluginId)}`;
116
+ const stored = (await options.database.getMeta(recordKey)) ??
117
+ null;
118
+ if (stored) {
119
+ const wrappingKey = await unlockBrowserSyncPrfCredential(stored, options.credentials);
120
+ try {
121
+ if (status.state === "unenrolled") {
122
+ await options.getSyncHost().enrollPluginCryptoWithWrappingKey(pluginId, wrappingKey, {
123
+ name: "external",
124
+ provider: "webauthn-prf",
125
+ keyId: stored.credentialId,
126
+ }, enrollment);
127
+ }
128
+ else {
129
+ await options
130
+ .getSyncHost()
131
+ .unlockPluginCryptoWithWrappingKey(pluginId, wrappingKey);
132
+ }
133
+ }
134
+ finally {
135
+ wrappingKey.fill(0);
136
+ }
137
+ return;
138
+ }
139
+ if (status.state === "unenrolled" &&
140
+ (await supportsBrowserWebAuthnPrf()) &&
141
+ (await options.interaction.confirmPasskeyEnrollment(pluginId))) {
142
+ const userId = randomBytes(32);
143
+ const created = await createBrowserSyncPrfCredential({
144
+ rpName: options.rpName,
145
+ rpId: options.rpId,
146
+ userId,
147
+ userName: options.userName ?? `sync:${pluginId}`,
148
+ credentials: options.credentials,
149
+ });
150
+ try {
151
+ await options.getSyncHost().enrollPluginCryptoWithWrappingKey(pluginId, created.wrappingKey, {
152
+ name: "external",
153
+ provider: "webauthn-prf",
154
+ keyId: created.credential.credentialId,
155
+ }, enrollment);
156
+ await options.database.setMeta(recordKey, created.credential);
157
+ }
158
+ finally {
159
+ created.wrappingKey.fill(0);
160
+ userId.fill(0);
161
+ }
162
+ return;
163
+ }
164
+ const passphrase = await options.interaction.requestPassphrase(pluginId, status.state === "unenrolled" ? "enroll" : "unlock");
165
+ if (passphrase == null)
166
+ throw new Error("Sync unlock was cancelled");
167
+ if (status.state === "unenrolled") {
168
+ await options
169
+ .getSyncHost()
170
+ .enrollPluginCrypto(pluginId, passphrase, enrollment);
171
+ }
172
+ else {
173
+ await options.getSyncHost().unlockPluginCrypto(pluginId, passphrase);
174
+ }
175
+ };
176
+ }
177
+ function toBase64Url(bytes) {
178
+ let binary = "";
179
+ for (const byte of bytes)
180
+ binary += String.fromCharCode(byte);
181
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
182
+ }
183
+ function encodeKeyPart(value) {
184
+ return toBase64Url(new TextEncoder().encode(value));
185
+ }
186
+ function fromBase64Url(value) {
187
+ const padded = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
188
+ return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
189
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Turso's prebuilt browser bundle already embeds its WASM as a complete data
3
+ * URL. Removing the redundant URL constructor prevents Vite's worker scanner
4
+ * from tokenizing the generated 15 MB module recursively during production
5
+ * builds, without changing the URL passed to fetch at runtime.
6
+ */
7
+ export declare function unwrapTursoBundleDataUrl(code: string, id: string): string;
8
+ export declare function tursoBundleBuildPlugin(): {
9
+ name: string;
10
+ enforce: "pre";
11
+ transform(this: import("rollup").TransformPluginContext, code: string, id: string): {
12
+ code: string;
13
+ map: null;
14
+ } | null;
15
+ };
@@ -0,0 +1,36 @@
1
+ const TURSO_BUNDLE_SUFFIX = "/node_modules/@tursodatabase/database-wasm/bundle/main.es.js";
2
+ const DATA_URL_PREFIX = 'new URL("data:application/wasm;base64,';
3
+ const URL_WRAPPER_PREFIX = "new URL(";
4
+ const URL_WRAPPER_SUFFIX = ", import.meta.url).href";
5
+ /**
6
+ * Turso's prebuilt browser bundle already embeds its WASM as a complete data
7
+ * URL. Removing the redundant URL constructor prevents Vite's worker scanner
8
+ * from tokenizing the generated 15 MB module recursively during production
9
+ * builds, without changing the URL passed to fetch at runtime.
10
+ */
11
+ export function unwrapTursoBundleDataUrl(code, id) {
12
+ const normalizedId = id.split("?", 1)[0]?.replaceAll("\\", "/") ?? id;
13
+ if (!normalizedId.endsWith(TURSO_BUNDLE_SUFFIX))
14
+ return code;
15
+ const expressionStart = code.indexOf(DATA_URL_PREFIX);
16
+ if (expressionStart < 0) {
17
+ return code.replaceAll("new Worker(", "new globalThis.Worker(");
18
+ }
19
+ const wrapperEnd = code.indexOf(URL_WRAPPER_SUFFIX, expressionStart);
20
+ if (wrapperEnd < 0) {
21
+ return code.replaceAll("new Worker(", "new globalThis.Worker(");
22
+ }
23
+ return (code.slice(0, expressionStart) +
24
+ code.slice(expressionStart + URL_WRAPPER_PREFIX.length, wrapperEnd) +
25
+ code.slice(wrapperEnd + URL_WRAPPER_SUFFIX.length)).replaceAll("new Worker(", "new globalThis.Worker(");
26
+ }
27
+ export function tursoBundleBuildPlugin() {
28
+ return {
29
+ name: "lapis-api-turso-bundle-data-url",
30
+ enforce: "pre",
31
+ transform(code, id) {
32
+ const transformed = unwrapTursoBundleDataUrl(code, id);
33
+ return transformed === code ? null : { code: transformed, map: null };
34
+ },
35
+ };
36
+ }
@@ -535,7 +535,7 @@ export declare class MetadataCache extends EventDispatcher<{
535
535
  private loadPrimarySnapshot;
536
536
  private hydrateDatabaseFromSnapshot;
537
537
  private reconcileSnapshotWithVault;
538
- getReconciliationFingerprint(): string;
538
+ getReconciliationFingerprint(acceptsFile?: (file: TFile) => boolean): string;
539
539
  getFileCache(file: TFile): CachedMetadata | null;
540
540
  getCache(path: string): CachedMetadata | null;
541
541
  getFileCacheAsync(file: TFile | string): Promise<CachedMetadata | null>;
@@ -586,12 +586,15 @@ export class MetadataCache extends EventDispatcher {
586
586
  /**
587
587
  * Return the current vault/parser manifest fingerprint used to validate the
588
588
  * durable metadata index and disposable indexed-consumer snapshots.
589
+ * Consumers may restrict the fingerprint to their accepted metadata files.
589
590
  */
590
- getReconciliationFingerprint() {
591
+ getReconciliationFingerprint(acceptsFile) {
591
592
  const fingerprint = new StreamingManifestFingerprint();
592
593
  for (const file of this.app.vault.iterateFiles()) {
593
594
  if (!this.processors.has(file.extension.toLowerCase()))
594
595
  continue;
596
+ if (acceptsFile && !acceptsFile(file))
597
+ continue;
595
598
  fingerprint.add([
596
599
  file.path,
597
600
  file.stat.mtime,
@@ -377,12 +377,14 @@
377
377
 
378
378
  [data-ui-component="editor"][data-ui-part="lint-rule"] {
379
379
  font: inherit;
380
- color: var(--ui-editor-lint-accent);
380
+ color: var(--foreground);
381
+ text-decoration: underline;
382
+ text-decoration-color: var(--ui-editor-lint-accent);
381
383
  text-underline-offset: 2px;
382
384
  }
383
385
 
384
386
  [data-ui-component="editor"][data-ui-part="lint-rule"]:hover {
385
- text-decoration: underline;
387
+ text-decoration-thickness: 2px;
386
388
  }
387
389
 
388
390
  [data-ui-component="editor"][data-ui-part="lint-copy"] {
@@ -444,6 +446,56 @@
444
446
  text-decoration: underline;
445
447
  }
446
448
 
449
+ .ui-editor-lint-quick-fix-menu {
450
+ z-index: calc(var(--ui-workspace-overlay-z-index, 50) + 10);
451
+ min-width: 13rem;
452
+ max-width: min(24rem, calc(100vw - 1rem));
453
+ border: 1px solid var(--ui-workspace-border, var(--border));
454
+ border-radius: var(--ui-workspace-radius-medium, 0.375rem);
455
+ padding: 0.25rem;
456
+ color: var(--ui-workspace-popover-foreground, var(--foreground));
457
+ background: var(--ui-workspace-popover, var(--popover, var(--secondary)));
458
+ box-shadow: var(
459
+ --ui-workspace-shadow,
460
+ 0 10px 24px rgb(0 0 0 / 0.18)
461
+ );
462
+ }
463
+
464
+ .ui-editor-lint-quick-fix-menu__item {
465
+ display: flex;
466
+ min-height: 2rem;
467
+ width: 100%;
468
+ align-items: center;
469
+ gap: 0.5rem;
470
+ border-radius: var(--ui-workspace-radius-small, 0.25rem);
471
+ padding: 0.375rem 0.5rem;
472
+ color: inherit;
473
+ font-family: var(--ui-editor-font-sans);
474
+ font-size: 0.875rem;
475
+ line-height: 1.25rem;
476
+ outline: none;
477
+ user-select: none;
478
+ }
479
+
480
+ .ui-editor-lint-quick-fix-menu__item:focus,
481
+ .ui-editor-lint-quick-fix-menu__item[data-highlighted] {
482
+ background: var(--ui-workspace-muted, var(--muted));
483
+ }
484
+
485
+ .ui-editor-lint-quick-fix-menu__item > svg {
486
+ width: 1rem;
487
+ height: 1rem;
488
+ flex: 0 0 1rem;
489
+ }
490
+
491
+ .ui-editor-lint-quick-fix-menu__label {
492
+ min-width: 0;
493
+ flex: 1 1 auto;
494
+ overflow: hidden;
495
+ text-overflow: ellipsis;
496
+ white-space: nowrap;
497
+ }
498
+
447
499
  /* Inline diagnostic expansion created by the lint tooltip's View Problem action. */
448
500
  [data-ui-component="editor"][data-ui-part="inline-problem"] {
449
501
  position: relative;
@@ -1,10 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { Button } from "@lapismd/design-core/shadcn/button";
3
3
  import { WorkspaceIcon } from "@lapismd/design-core/workspace/icon";
4
- import { WorkspaceMenuItems } from "@lapismd/design-core/workspace/menu";
5
4
  import { DropdownMenu } from "bits-ui";
6
5
  import {
7
- createLintQuickFixMenu,
8
6
  splitLintTooltipActions,
9
7
  type LintTooltipAction,
10
8
  } from "./lint-tooltip-actions";
@@ -35,7 +33,6 @@
35
33
  const { viewProblem, quickFixActions } = $derived(
36
34
  splitLintTooltipActions(actions),
37
35
  );
38
- const quickFixMenu = $derived(createLintQuickFixMenu(quickFixActions));
39
36
  const showFooter = $derived(
40
37
  Boolean(viewProblem) || quickFixActions.length > 0,
41
38
  );
@@ -54,6 +51,10 @@
54
51
  function stopRowActivation(event: Event) {
55
52
  event.stopPropagation();
56
53
  }
54
+
55
+ function selectQuickFix(action: LintTooltipAction, event: MouseEvent) {
56
+ action.onClick(event);
57
+ }
57
58
  </script>
58
59
 
59
60
  <div
@@ -139,7 +140,7 @@
139
140
  </DropdownMenu.Trigger>
140
141
  <DropdownMenu.Portal>
141
142
  <DropdownMenu.Content
142
- class="ui-workspace-menu__content"
143
+ class="ui-editor-lint-quick-fix-menu"
143
144
  data-ui-component="workspace-menu"
144
145
  data-ui-part="content"
145
146
  data-lint-quick-fix-menu=""
@@ -150,7 +151,19 @@
150
151
  collisionBoundary={[]}
151
152
  onpointerdown={stopRowActivation}
152
153
  >
153
- <WorkspaceMenuItems menu={quickFixMenu} />
154
+ {#each quickFixActions as action, index (`${action.name}-${index}`)}
155
+ <DropdownMenu.Item
156
+ class="ui-editor-lint-quick-fix-menu__item"
157
+ data-ui-component="workspace-menu"
158
+ data-ui-part="item"
159
+ onclick={(event) => selectQuickFix(action, event)}
160
+ >
161
+ <WorkspaceIcon name="lightbulb" />
162
+ <span class="ui-editor-lint-quick-fix-menu__label">
163
+ {action.name}
164
+ </span>
165
+ </DropdownMenu.Item>
166
+ {/each}
154
167
  </DropdownMenu.Content>
155
168
  </DropdownMenu.Portal>
156
169
  </DropdownMenu.Root>
@@ -1,4 +1,3 @@
1
- import { WorkspaceMenu } from "@lapismd/design-core/workspace/core";
2
1
  export declare const VIEW_PROBLEM_ACTION = "View Problem";
3
2
  export type LintTooltipAction = {
4
3
  name: string;
@@ -8,4 +7,3 @@ export declare function splitLintTooltipActions(actions: readonly LintTooltipAct
8
7
  viewProblem: LintTooltipAction | undefined;
9
8
  quickFixActions: LintTooltipAction[];
10
9
  };
11
- export declare function createLintQuickFixMenu(actions: readonly LintTooltipAction[]): WorkspaceMenu;
@@ -1,4 +1,3 @@
1
- import { WorkspaceMenu } from "@lapismd/design-core/workspace/core";
2
1
  export const VIEW_PROBLEM_ACTION = "View Problem";
3
2
  export function splitLintTooltipActions(actions) {
4
3
  return {
@@ -6,16 +5,3 @@ export function splitLintTooltipActions(actions) {
6
5
  quickFixActions: actions.filter((action) => action.name !== VIEW_PROBLEM_ACTION),
7
6
  };
8
7
  }
9
- export function createLintQuickFixMenu(actions) {
10
- const menu = new WorkspaceMenu();
11
- for (const action of actions) {
12
- menu.addItem((item) => item
13
- .setTitle(action.name)
14
- .setIcon("lightbulb")
15
- .setSection("fix")
16
- .onClick((event) => {
17
- action.onClick(event instanceof MouseEvent ? event : new MouseEvent("click"));
18
- }));
19
- }
20
- return menu;
21
- }
@@ -1,8 +1,8 @@
1
- import { Facet } from "@codemirror/state";
1
+ import { Facet, StateEffect, } from "@codemirror/state";
2
2
  import { hoverTooltip, ViewPlugin, } from "@codemirror/view";
3
3
  import { editorViewField, fromPosition, toPosition, } from "../../../editor.svelte";
4
4
  import { lapisCodeMirrorAutocomplete } from "../extensions/autocomplete";
5
- import { lapisCodeMirrorLint, lintGutter, mapToLapisLintDiagnostic, markdownlintRuleUrl, setDiagnostics, } from "../extensions/lint";
5
+ import { lapisCodeMirrorLint, lintGutter, mapToLapisLintDiagnostic, markdownlintRuleUrl, setDiagnostics, forceLinting, } from "../extensions/lint";
6
6
  import { createLanguageServiceHoverDom } from "./dom";
7
7
  export const languageServiceDocumentContext = Facet.define({
8
8
  combine(values) {
@@ -10,6 +10,7 @@ export const languageServiceDocumentContext = Facet.define({
10
10
  },
11
11
  });
12
12
  const trackedOpenDocuments = new WeakMap();
13
+ const diagnosticsInvalidatedEffect = StateEffect.define();
13
14
  export function languageServiceExtensions(options = {}) {
14
15
  const extensions = [];
15
16
  const wantsDiagnostics = options.diagnostics ?? true;
@@ -18,6 +19,9 @@ export function languageServiceExtensions(options = {}) {
18
19
  extensions.push(openDocumentTrackingExtension(options), ...lapisCodeMirrorLint({
19
20
  source: async (view) => collectLanguageServiceDiagnostics(view, options),
20
21
  gutter: wantsGutter ? undefined : false,
22
+ lint: {
23
+ needsRefresh: (update) => update.transactions.some((transaction) => transaction.effects.some((effect) => effect.is(diagnosticsInvalidatedEffect))),
24
+ },
21
25
  }));
22
26
  }
23
27
  else if (wantsGutter) {
@@ -33,10 +37,26 @@ export function languageServiceExtensions(options = {}) {
33
37
  }
34
38
  function openDocumentTrackingExtension(options) {
35
39
  return ViewPlugin.define((view) => {
40
+ let subscribedManager;
41
+ let unsubscribeInvalidation;
36
42
  const sync = (nextView) => {
37
43
  const resolved = resolveDocumentContext(nextView, options.languageId);
38
44
  if (!resolved)
39
45
  return;
46
+ const manager = resolved.info.app.languageServices;
47
+ if (subscribedManager !== manager) {
48
+ unsubscribeInvalidation?.();
49
+ subscribedManager = manager;
50
+ unsubscribeInvalidation = manager.onDiagnosticsInvalidated((languages) => {
51
+ const current = resolveDocumentContext(view, options.languageId);
52
+ if (current && languages.includes(current.document.languageId)) {
53
+ view.dispatch({
54
+ effects: diagnosticsInvalidatedEffect.of(undefined),
55
+ });
56
+ forceLinting(view);
57
+ }
58
+ });
59
+ }
40
60
  retainOpenDocument(nextView, resolved.document.uri, resolved.info.app.languageServices);
41
61
  };
42
62
  sync(view);
@@ -45,6 +65,7 @@ function openDocumentTrackingExtension(options) {
45
65
  sync(update.view);
46
66
  },
47
67
  destroy() {
68
+ unsubscribeInvalidation?.();
48
69
  trackedOpenDocuments.get(view)?.release();
49
70
  trackedOpenDocuments.delete(view);
50
71
  },
@@ -6,6 +6,6 @@ type $$ComponentProps = ButtonProps & {
6
6
  ref?: HTMLButtonElement | null;
7
7
  label?: string;
8
8
  };
9
- declare const IconList: import("svelte").Component<$$ComponentProps, {}, "ref" | "value">;
9
+ declare const IconList: import("svelte").Component<$$ComponentProps, {}, "value" | "ref">;
10
10
  type IconList = ReturnType<typeof IconList>;
11
11
  export default IconList;
@@ -20,6 +20,6 @@ export type MultipleSelectProps = SharedSelectProps & {
20
20
  onValueChange?: (value: string[]) => void;
21
21
  };
22
22
  export type SelectProps = SingleSelectProps | MultipleSelectProps;
23
- declare const Select: import("svelte").Component<SelectProps, {}, "ref" | "value">;
23
+ declare const Select: import("svelte").Component<SelectProps, {}, "value" | "ref">;
24
24
  type Select = ReturnType<typeof Select>;
25
25
  export default Select;
@@ -30,6 +30,8 @@ import { DailyDocumentProviderRegistry } from "./daily-document-provider";
30
30
  import { AppToolRegistry } from "./agent-tools";
31
31
  import { MarkdownExtensionRegistry } from "./markdown-extension-registry";
32
32
  import { MarkdownFileSurfaceRegistry } from "./markdown-file-surface-registry";
33
+ import { NostrSignerHost, type NostrSignerBroker } from "./nostr-signer";
34
+ import { SyncHostService, type SyncHostServiceOptions } from "./sync-host";
33
35
  export interface AppWorkspaceShellApplicationProperties {
34
36
  /** Application name shown by the shell's About surface. */
35
37
  name?: string;
@@ -60,9 +62,14 @@ export type AppStateProperties = {
60
62
  createCommunityPluginDependencyResolver?: PluginDependencyResolverFactory;
61
63
  pluginAssetServer?: PluginAssetServer;
62
64
  pluginDistributionOptions?: AppPluginDistributionOptions;
65
+ /** Host-owned Nostr account, keychain, and remote-signer implementation. */
66
+ nostrSignerBroker?: NostrSignerBroker;
67
+ syncHost?: SyncHostServiceOptions;
63
68
  workspaceShell?: AppWorkspaceShellProperties;
64
69
  markdownRenderer: (markdown: string, el: HTMLElement, sourcePath: string, component: Component) => Promise<void>;
65
70
  };
71
+ /** Renderer-safe application properties exposed to plugins and views. */
72
+ export type AppPublicProperties = Pick<AppStateProperties, "version" | "configPath" | "workspaceShell" | "markdownRenderer">;
66
73
  export type AppPluginDistributionOptions = Omit<DefaultPluginDistributionManagerOptions, "adapter" | "appVersion" | "platform" | "workspaceTrusted" | "pluginManager">;
67
74
  export type ExtType = Extension | ((context: Record<string, any>) => Extension);
68
75
  /**
@@ -146,7 +153,7 @@ export declare class SecretStorage {
146
153
  * @public
147
154
  */
148
155
  export declare class App {
149
- readonly props: AppStateProperties;
156
+ readonly props: AppPublicProperties;
150
157
  readonly i18n: LocalizationManager;
151
158
  readonly workspace: Workspace;
152
159
  readonly vault: Vault;
@@ -166,6 +173,7 @@ export declare class App {
166
173
  readonly urls: AppUrlService;
167
174
  readonly workspaceTrust: WorkspaceTrustService;
168
175
  readonly pluginDistribution: DefaultPluginDistributionManager;
176
+ readonly nostr: NostrSignerHost;
169
177
  mardownPostProcessor: Array<MarkdownPostProcessor>;
170
178
  mardownCodeBlockPostProcessor: Record<string, Array<MarkdownPostProcessor>>;
171
179
  markdownDirectiveRenderers: Record<string, MarkdownDirectiveRenderer>;
@@ -239,6 +247,8 @@ export declare class App {
239
247
  setting(id: "options" | "core-plugins" | "community-plugins"): import("./settings.svelte").SettingGroup;
240
248
  }
241
249
  export type AppState = App;
250
+ /** Transfer the mounting host's sync controller exactly once. */
251
+ export declare function takeHostSyncService(app: App): SyncHostService;
242
252
  declare global {
243
253
  var app: App;
244
254
  }