@markii/host 0.0.1 → 0.17.0-next.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/README.md +20 -2
- package/dist/browser-worker.js +639 -0
- package/dist/browser.d.ts +61 -0
- package/dist/browser.js +172 -0
- package/dist/conformance/host/01-plain-open/expected.json +5 -0
- package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
- package/dist/conformance/host/01-plain-open/scenario.json +6 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
- package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
- package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
- package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
- package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
- package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
- package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
- package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
- package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
- package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
- package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
- package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
- package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
- package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
- package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
- package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
- package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
- package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
- package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
- package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
- package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
- package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
- package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
- package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
- package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
- package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
- package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
- package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
- package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
- package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
- package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
- package/dist/conformance/host/14-remove-pack/expected.json +14 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
- package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
- package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
- package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
- package/dist/conformance/host/16-pack-trust/expected.json +37 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
- package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
- package/dist/conformance/host/README.md +221 -0
- package/dist/conformance.d.ts +2 -0
- package/dist/conformance.js +5 -0
- package/dist/diagnostics/render-diagnostics.d.ts +41 -0
- package/dist/diagnostics/render-diagnostics.js +46 -0
- package/dist/export/cascade.d.ts +98 -0
- package/dist/export/cascade.js +159 -0
- package/dist/export/export-zip.d.ts +19 -0
- package/dist/export/export-zip.js +38 -0
- package/dist/export/image-embed.d.ts +126 -0
- package/dist/export/image-embed.js +274 -0
- package/dist/export/note-export.d.ts +332 -0
- package/dist/export/note-export.js +366 -0
- package/dist/export/note-links.d.ts +83 -0
- package/dist/export/note-links.js +241 -0
- package/dist/glue.wasm +0 -0
- package/dist/host/adapter.d.ts +218 -0
- package/dist/host/adapter.js +9 -0
- package/dist/host/cascade-export-behavior.d.ts +120 -0
- package/dist/host/cascade-export-behavior.js +151 -0
- package/dist/host/conformance-runner.d.ts +159 -0
- package/dist/host/conformance-runner.js +632 -0
- package/dist/host/create-host.d.ts +251 -0
- package/dist/host/create-host.js +329 -0
- package/dist/host/editor-behavior.d.ts +153 -0
- package/dist/host/editor-behavior.js +223 -0
- package/dist/host/export/md-plain.d.ts +15 -0
- package/dist/host/export/md-plain.js +125 -0
- package/dist/host/export-behavior.d.ts +111 -0
- package/dist/host/export-behavior.js +257 -0
- package/dist/host/labels.d.ts +23 -0
- package/dist/host/labels.js +32 -0
- package/dist/host/pack-archive.d.ts +30 -0
- package/dist/host/pack-archive.js +62 -0
- package/dist/host/pack-install.d.ts +87 -0
- package/dist/host/pack-install.js +186 -0
- package/dist/host/pack-load.d.ts +71 -0
- package/dist/host/pack-load.js +57 -0
- package/dist/host/pack-remove.d.ts +21 -0
- package/dist/host/pack-remove.js +65 -0
- package/dist/host/pack-trust-behavior.d.ts +79 -0
- package/dist/host/pack-trust-behavior.js +262 -0
- package/dist/host/refresh-interval.d.ts +37 -0
- package/dist/host/refresh-interval.js +59 -0
- package/dist/host/run-behavior.d.ts +88 -0
- package/dist/host/run-behavior.js +189 -0
- package/dist/host/run-diagnostics.d.ts +26 -0
- package/dist/host/run-diagnostics.js +43 -0
- package/dist/host/script-execution.d.ts +72 -0
- package/dist/host/script-execution.js +68 -0
- package/dist/host/terminal-packs.d.ts +39 -0
- package/dist/host/terminal-packs.js +51 -0
- package/dist/html-engine.d.ts +2 -0
- package/dist/html-engine.js +29 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +69 -0
- package/dist/insert/component-catalog.d.ts +23 -0
- package/dist/insert/component-catalog.js +96 -0
- package/dist/node-fs.d.ts +14 -0
- package/dist/node-fs.js +70 -0
- package/dist/node-worker.js +20657 -0
- package/dist/node.d.ts +6 -0
- package/dist/node.js +13 -0
- package/dist/package.json +3 -0
- package/dist/packs/discover.d.ts +119 -0
- package/dist/packs/discover.js +266 -0
- package/dist/packs/pack-diagnostics.d.ts +79 -0
- package/dist/packs/pack-diagnostics.js +103 -0
- package/dist/packs/pack-paths.d.ts +53 -0
- package/dist/packs/pack-paths.js +105 -0
- package/dist/packs/pack-render-registry.d.ts +82 -0
- package/dist/packs/pack-render-registry.js +182 -0
- package/dist/packs/pack-scripts.d.ts +20 -0
- package/dist/packs/pack-scripts.js +96 -0
- package/dist/packs/pack-trust.d.ts +57 -0
- package/dist/packs/pack-trust.js +111 -0
- package/dist/packs/prebuilt.d.ts +49 -0
- package/dist/packs/prebuilt.js +97 -0
- package/dist/path-util.d.ts +24 -0
- package/dist/path-util.js +82 -0
- package/dist/run/browser-isolate.d.ts +45 -0
- package/dist/run/browser-isolate.js +85 -0
- package/dist/run/bundle-document.d.ts +29 -0
- package/dist/run/bundle-document.js +40 -0
- package/dist/run/bundle-run.d.ts +188 -0
- package/dist/run/bundle-run.js +232 -0
- package/dist/run/grant-flow.d.ts +232 -0
- package/dist/run/grant-flow.js +390 -0
- package/dist/run/ip-address.d.ts +41 -0
- package/dist/run/ip-address.js +255 -0
- package/dist/run/isolate.d.ts +59 -0
- package/dist/run/isolate.js +22 -0
- package/dist/run/lua-resolver.d.ts +21 -0
- package/dist/run/lua-resolver.js +63 -0
- package/dist/run/net-bridge-worker.d.ts +26 -0
- package/dist/run/net-bridge-worker.js +66 -0
- package/dist/run/net-bridge.d.ts +56 -0
- package/dist/run/net-bridge.js +59 -0
- package/dist/run/net-pinning.d.ts +82 -0
- package/dist/run/net-pinning.js +137 -0
- package/dist/run/net-provider.d.ts +72 -0
- package/dist/run/net-provider.js +303 -0
- package/dist/run/node-isolate.d.ts +22 -0
- package/dist/run/node-isolate.js +68 -0
- package/dist/run/run-flow.d.ts +186 -0
- package/dist/run/run-flow.js +323 -0
- package/dist/run/run-host.d.ts +107 -0
- package/dist/run/run-host.js +241 -0
- package/dist/run/run-job.d.ts +130 -0
- package/dist/run/run-job.js +279 -0
- package/dist/run/run-progress.d.ts +55 -0
- package/dist/run/run-progress.js +42 -0
- package/dist/run/run-trace.d.ts +44 -0
- package/dist/run/run-trace.js +36 -0
- package/dist/run/script-requirements.d.ts +98 -0
- package/dist/run/script-requirements.js +212 -0
- package/dist/run/snapshot-storage.d.ts +25 -0
- package/dist/run/snapshot-storage.js +31 -0
- package/dist/run/stale-values.d.ts +25 -0
- package/dist/run/stale-values.js +32 -0
- package/dist/run/worker-entry-browser.d.ts +62 -0
- package/dist/run/worker-entry-browser.js +172 -0
- package/dist/run/worker-entry.d.ts +1 -0
- package/dist/run/worker-entry.js +98 -0
- package/dist/values-failure.d.ts +17 -0
- package/dist/values-failure.js +1 -0
- package/dist/values-merge.d.ts +39 -0
- package/dist/values-merge.js +33 -0
- package/package.json +80 -4
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a host's pack-folder setting entries to absolute folder paths.
|
|
3
|
+
* Pure (paths in, a base directory in, absolute paths out) so it is
|
|
4
|
+
* unit-testable without a real workspace/vault.
|
|
5
|
+
*
|
|
6
|
+
* `baseDir` is deliberately a generic name: what it MEANS differs per host
|
|
7
|
+
* (VS Code resolves a relative entry against the open workspace folder,
|
|
8
|
+
* since `markii.packs` is a user-scoped setting; an Obsidian plugin
|
|
9
|
+
* resolves it against the open vault, since its pack-folder list is a
|
|
10
|
+
* device-local setting that can be opened against several vaults) but the
|
|
11
|
+
* resolution RULE is identical, so only the base directory's identity is a
|
|
12
|
+
* host concern — reading the setting itself, and naming what "relative"
|
|
13
|
+
* means in a diagnostic message, stays in each host's own app code.
|
|
14
|
+
*
|
|
15
|
+
* DECISION: a relative entry is resolved against
|
|
16
|
+
* `baseDir` as given by the caller; an absolute entry is used as-is. This
|
|
17
|
+
* matches how a user reads the setting's own description: "this pack
|
|
18
|
+
* folder, relative to my project/vault".
|
|
19
|
+
*/
|
|
20
|
+
import { isAbsolutePath, joinPath } from '../path-util.js';
|
|
21
|
+
/**
|
|
22
|
+
* Expands a leading `~` in `entry` against `homeDir` — `~` alone, or
|
|
23
|
+
* `~/...`/`~\...` (both separators accepted regardless of host platform, so
|
|
24
|
+
* a setting written on one OS still expands sensibly if synced to
|
|
25
|
+
* another). Any other entry (including one that merely CONTAINS a `~`
|
|
26
|
+
* elsewhere, or has no `homeDir` to expand against) passes through
|
|
27
|
+
* unchanged. This is convenience, not path resolution: the result still
|
|
28
|
+
* goes through the ordinary absolute/relative handling in
|
|
29
|
+
* `resolvePackPaths` below, exactly like a path the user typed out in
|
|
30
|
+
* full.
|
|
31
|
+
*/
|
|
32
|
+
function expandHome(entry, homeDir) {
|
|
33
|
+
if (homeDir === undefined)
|
|
34
|
+
return entry;
|
|
35
|
+
if (entry === '~')
|
|
36
|
+
return homeDir;
|
|
37
|
+
if (entry.startsWith('~/') || entry.startsWith('~\\')) {
|
|
38
|
+
return joinPath(homeDir, entry.slice(2));
|
|
39
|
+
}
|
|
40
|
+
return entry;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolves each entry in `packs` to an absolute path. `homeDir` is a plain
|
|
44
|
+
* parameter (not read via `os.homedir()` internally) so this stays testable
|
|
45
|
+
* against a fixed value instead of the real host's home directory; the real
|
|
46
|
+
* caller (each host's own `pack-context.ts`) passes `os.homedir()` itself.
|
|
47
|
+
* Omitted (or `undefined`) disables `~` expansion entirely — a `~` entry is
|
|
48
|
+
* then simply relative like any other.
|
|
49
|
+
*
|
|
50
|
+
* - A `~`/`~/...` entry is expanded against `homeDir` FIRST (`expandHome`)
|
|
51
|
+
* — this makes an absolute path pleasant to write, which matters because
|
|
52
|
+
* a relative entry can mean a different folder in every window/vault it
|
|
53
|
+
* happens to be open in, so an absolute (or `~`-relative) entry is the
|
|
54
|
+
* only spelling that means the same folder everywhere.
|
|
55
|
+
* - An entry that is (now) already absolute passes through UNCHANGED —
|
|
56
|
+
* never re-rooted, and never rejected for pointing outside `baseDir`,
|
|
57
|
+
* since an absolute path is the user explicitly naming a folder wherever
|
|
58
|
+
* it is (docs/packs.md: packs are installed into the application, not
|
|
59
|
+
* jailed the way a bundle's contents are).
|
|
60
|
+
* - A relative entry (still relative after `~` expansion) is joined
|
|
61
|
+
* against `baseDir` and NORMALIZED (`path.normalize` via `path.join`,
|
|
62
|
+
* which collapses `..` segments syntactically). A relative entry that
|
|
63
|
+
* climbs out of `baseDir` via `..` is intentionally still honored, for
|
|
64
|
+
* the same reason an absolute entry is: this setting is a user-authored
|
|
65
|
+
* trust decision, not a content-derived path that needs jailing — unlike
|
|
66
|
+
* a bundle's `scripts/`/`assets/` paths (`@markii/bundle`'s
|
|
67
|
+
* `normalizeBundlePath`), which DO get jailed because they come from
|
|
68
|
+
* potentially-hostile bundle content, not from the user directly editing
|
|
69
|
+
* their own settings.
|
|
70
|
+
* - `baseDir === undefined` (no workspace/vault open) leaves every relative
|
|
71
|
+
* entry UNRESOLVED (returned as-is, unjoined) rather than guessing a
|
|
72
|
+
* base — a relative entry with nothing open cannot mean anything, so the
|
|
73
|
+
* caller's later folder-read simply fails to find a `pack.json` there
|
|
74
|
+
* and `discoverPacks` quietly skips it, the same cleanliness posture as
|
|
75
|
+
* any other unreadable folder.
|
|
76
|
+
*
|
|
77
|
+
* Never throws — every input is a configuration string a user typed by
|
|
78
|
+
* hand, and a malformed one should degrade to "not found", not to a crash.
|
|
79
|
+
*/
|
|
80
|
+
export function resolvePackPaths(packs, baseDir, homeDir) {
|
|
81
|
+
return packs.map((entry) => {
|
|
82
|
+
const expanded = expandHome(entry, homeDir);
|
|
83
|
+
if (isAbsolutePath(expanded))
|
|
84
|
+
return expanded;
|
|
85
|
+
if (baseDir === undefined)
|
|
86
|
+
return expanded;
|
|
87
|
+
return joinPath(baseDir, expanded);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The detection half of the relative-entry diagnostics: the ORIGINAL
|
|
92
|
+
* (unexpanded) entries in
|
|
93
|
+
* `packs` that are relative once `~` expansion is accounted for — i.e.
|
|
94
|
+
* exactly the entries `resolvePackPaths` resolves against `baseDir` rather
|
|
95
|
+
* than using as-is. A relative entry can silently mean a different folder
|
|
96
|
+
* in every window/vault it happens to be open in, so a caller surfaces
|
|
97
|
+
* these to its own diagnostics surface, in its own wording (each host
|
|
98
|
+
* names its setting and the ambiguity differently — see this module's top
|
|
99
|
+
* doc comment); `resolvePackPaths` keeps resolving them exactly as it
|
|
100
|
+
* always has: this function only ever adds a warning, never changes
|
|
101
|
+
* behavior.
|
|
102
|
+
*/
|
|
103
|
+
export function relativePackEntries(packs, homeDir) {
|
|
104
|
+
return packs.filter((entry) => !isAbsolutePath(expandHome(entry, homeDir)));
|
|
105
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { PackManifest } from '@markii/pack';
|
|
2
|
+
/**
|
|
3
|
+
* The renderer-specific half of building a pack-aware registry: the
|
|
4
|
+
* renderer's own `loadPack`, `createRegistry` and `mergeRegistries`. A
|
|
5
|
+
* React host passes `@markii/react`'s `reactPackEngine`; the host layer
|
|
6
|
+
* itself names no renderer.
|
|
7
|
+
*/
|
|
8
|
+
export interface PackRegistryEngine<TRegistry extends object = object, TEntry = unknown> {
|
|
9
|
+
loadPack(manifest: PackManifest, componentModules: Record<string, TEntry>): {
|
|
10
|
+
registry: TRegistry;
|
|
11
|
+
dropped?: {
|
|
12
|
+
name: string;
|
|
13
|
+
engine: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
createRegistry(entries?: Record<string, TEntry>): TRegistry;
|
|
17
|
+
mergeRegistries(...registries: TRegistry[]): TRegistry;
|
|
18
|
+
}
|
|
19
|
+
/** One registration a compiled pack script queued by calling `window.__markiiRegisterPack(manifestJson, componentModules)` (see `./pack-build.ts`'s top doc comment for the full registration convention). Structurally identical across every host that satisfies the convention. */
|
|
20
|
+
export interface QueuedPackRegistration {
|
|
21
|
+
readonly manifestJson: unknown;
|
|
22
|
+
readonly componentModules: unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* One composed directive name two DIFFERENT packs both claimed. Under
|
|
26
|
+
* `@markii/pack`'s current underscore join (issue #19) this cannot arise
|
|
27
|
+
* from two packs' names and local names composing to the same string — the
|
|
28
|
+
* join is bijective (see `namespace.ts`'s `composeDirectiveName` doc
|
|
29
|
+
* comment) — so this is a defense-in-depth invariant, not a reachable case
|
|
30
|
+
* of ordinary pack composition. It stays because a hand-written pack
|
|
31
|
+
* registration script (or a future regression in the composition rule)
|
|
32
|
+
* could still hand `buildRenderRegistry` two registrations that carry the
|
|
33
|
+
* identical composed name; when that happens the FIRST claimant keeps the
|
|
34
|
+
* name and the later one is dropped, exactly like a malformed registration.
|
|
35
|
+
*/
|
|
36
|
+
export interface DuplicateComposedName {
|
|
37
|
+
readonly composedName: string;
|
|
38
|
+
/** The pack whose component kept the name. */
|
|
39
|
+
readonly keptPack: string;
|
|
40
|
+
/** The pack whose component was skipped because the name was already taken. */
|
|
41
|
+
readonly skippedPack: string;
|
|
42
|
+
}
|
|
43
|
+
/** What `buildRenderRegistry` reports alongside the merged registry — developer-facing diagnostics for a caller to fold into its own surface, never shown as page content. */
|
|
44
|
+
export interface BuildRenderRegistryResult<TRegistry extends object = object> {
|
|
45
|
+
readonly registry: TRegistry;
|
|
46
|
+
/** One line per malformed registration, dropped rather than installed. */
|
|
47
|
+
readonly invalidReasons: readonly string[];
|
|
48
|
+
/** Non-empty only when two or more validated registrations shared a namespace — the WHOLE install was then rejected (docs/packs.md's install-time rejection rule) and `registry` falls back to `defaultRegistry` alone. */
|
|
49
|
+
readonly collisions: readonly string[];
|
|
50
|
+
/** One entry per composed directive name claimed by two different packs — see `DuplicateComposedName`. Always empty for registrations that went through ordinary composition; kept as an executable invariant, not dead code (see `./pack-render-registry.test.ts`'s constructed-collision test). */
|
|
51
|
+
readonly duplicateComposedNames: readonly DuplicateComposedName[];
|
|
52
|
+
/**
|
|
53
|
+
* One entry per validated, non-colliding pack whose manifest declares an
|
|
54
|
+
* engine `@markii/react`'s `loadPack` cannot run. Before
|
|
55
|
+
* this field existed, such a pack installed as an empty contribution
|
|
56
|
+
* with no record anywhere that it had even been attempted — every
|
|
57
|
+
* directive under its namespace fell through to the ordinary
|
|
58
|
+
* unknown-component fallback, indistinguishable from a directive that
|
|
59
|
+
* was never a pack component at all. Always empty for an ordinary
|
|
60
|
+
* all-react-engine install.
|
|
61
|
+
*/
|
|
62
|
+
readonly droppedEngines: readonly {
|
|
63
|
+
name: string;
|
|
64
|
+
engine: string;
|
|
65
|
+
}[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Builds the render registry: `defaultRegistry` merged with every
|
|
69
|
+
* successfully validated, non-colliding pack registration in `queued`.
|
|
70
|
+
* Never throws:
|
|
71
|
+
*
|
|
72
|
+
* - an individual malformed registration is dropped (reason recorded, never
|
|
73
|
+
* shown on the page);
|
|
74
|
+
* - a namespace collision between two loaded packs (the same pack name
|
|
75
|
+
* registered twice) rejects the WHOLE install (matching docs/packs.md's
|
|
76
|
+
* install-time rejection rule) and this function falls back to
|
|
77
|
+
* `defaultRegistry` alone;
|
|
78
|
+
* - a composed-directive-name collision between two DIFFERENTLY named packs
|
|
79
|
+
* (see `DuplicateComposedName`) drops only the later claimant's
|
|
80
|
+
* component, not the whole pack.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildRenderRegistry<TRegistry extends object, TEntry>(queued: readonly QueuedPackRegistration[], defaultRegistry: TRegistry, engine: PackRegistryEngine<TRegistry, TEntry>): BuildRenderRegistryResult<TRegistry>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates a batch of `QueuedPackRegistration`s and merges them into a
|
|
3
|
+
* render `Registry` on top of a base registry, using `@markii/react`'s
|
|
4
|
+
* `loadPack` per pack (namespace-collision rejection reimplemented here so
|
|
5
|
+
* this module can also guard against a same-composed-name collision
|
|
6
|
+
* between two differently named packs — see `DuplicateComposedName` and
|
|
7
|
+
* `mergePacksKeepingFirstClaim`).
|
|
8
|
+
*
|
|
9
|
+
* Every entry here crossed a boundary from a SEPARATELY EVALUATED script (a
|
|
10
|
+
* pack's compiled artifact, not code a host's own bundle compiled) — same
|
|
11
|
+
* trust as that bundle once loaded (docs/security.md: "packs are
|
|
12
|
+
* user-installed/trusted"), but still validated structurally before use,
|
|
13
|
+
* because "trusted" does not mean "guaranteed well-formed": a buggy or
|
|
14
|
+
* half-built pack script must degrade the SAME way an unsupported engine or
|
|
15
|
+
* a missing component module already does in `@markii/react`'s `loadPack`
|
|
16
|
+
* (an empty contribution, never a crash), not bring down the rest of a
|
|
17
|
+
* preview.
|
|
18
|
+
*
|
|
19
|
+
* Host-neutral by design: this module takes an already-collected array of
|
|
20
|
+
* queued registrations as a plain parameter, and returns the outcome as
|
|
21
|
+
* data (never logs or throws), so a host wires up wherever those
|
|
22
|
+
* registrations came from — VS Code reads `window.__markiiPackRegistrations`
|
|
23
|
+
* off its webview and logs the result to its output channel; an Obsidian
|
|
24
|
+
* plugin evaluates each pack's compiled script in-process
|
|
25
|
+
* (`collectPackRegistrations`) and folds the result into its own
|
|
26
|
+
* diagnostics. Reading a host-specific global and deciding where to log a
|
|
27
|
+
* diagnostic line both stay in that host's own app code.
|
|
28
|
+
*/
|
|
29
|
+
import { detectNamespaceCollisions, parsePackManifest } from '@markii/pack';
|
|
30
|
+
function isPlainObject(value) {
|
|
31
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
function hasOwn(value, key) {
|
|
34
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
35
|
+
}
|
|
36
|
+
/** Structurally validates one `componentModules` object: every OWN entry has a function `component` and an optional boolean `inline`. Read with `Object.hasOwn` only — the same hostile-map discipline `@markii/pack`/`@markii/react` use throughout, so a `componentModules` object with a poisoned `__proto__` can't leak an inherited value in as a registered component. */
|
|
37
|
+
function isPackComponentModules(value) {
|
|
38
|
+
if (!isPlainObject(value))
|
|
39
|
+
return false;
|
|
40
|
+
for (const key of Object.keys(value)) {
|
|
41
|
+
if (!hasOwn(value, key))
|
|
42
|
+
continue;
|
|
43
|
+
const entry = value[key];
|
|
44
|
+
if (!isPlainObject(entry))
|
|
45
|
+
return false;
|
|
46
|
+
if (typeof entry.component !== 'function')
|
|
47
|
+
return false;
|
|
48
|
+
if (hasOwn(entry, 'inline') && typeof entry.inline !== 'boolean') {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One queued entry, validated into a `PackToInstall`, or `undefined` if
|
|
56
|
+
* either half fails validation — the reason is returned rather than logged
|
|
57
|
+
* directly, so the caller can fold it into its own diagnostics the same way
|
|
58
|
+
* every other pack failure is reported.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately NOT exported, including from `../browser.ts`: every caller,
|
|
61
|
+
* VS Code's webview included, reaches this through `buildRenderRegistry`,
|
|
62
|
+
* which runs it per queued entry. A host reads its own registration queue
|
|
63
|
+
* off its own global and hands over the batch; validating that batch is
|
|
64
|
+
* this module's job, and keeping the per-entry step private means there is
|
|
65
|
+
* one entry point into it rather than two.
|
|
66
|
+
*/
|
|
67
|
+
function toPackToInstall(entry, index) {
|
|
68
|
+
if (typeof entry.manifestJson !== 'string') {
|
|
69
|
+
return {
|
|
70
|
+
invalidReason: `pack registration #${String(index)} did not provide a manifest JSON string; ignored.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const parsed = parsePackManifest(entry.manifestJson);
|
|
74
|
+
if (!parsed.ok) {
|
|
75
|
+
return {
|
|
76
|
+
invalidReason: `pack registration #${String(index)}'s manifest is invalid (${parsed.errors.join('; ')}); ignored.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (!isPackComponentModules(entry.componentModules)) {
|
|
80
|
+
return {
|
|
81
|
+
invalidReason: `pack "${parsed.manifest.name}"'s registered components are malformed; ignored.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const manifest = parsed.manifest;
|
|
85
|
+
return { pack: { manifest, componentModules: entry.componentModules } };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Loads every pack's own registry (`@markii/react`'s `loadPack`, which
|
|
89
|
+
* already gates on engine and skips an unsatisfied component) and merges
|
|
90
|
+
* them onto `base` left to right, but — unlike `@markii/react`'s ordinary
|
|
91
|
+
* `mergeRegistries` last-wins semantics — keeps the FIRST pack to claim a
|
|
92
|
+
* given composed directive name and skips any later pack that claims the
|
|
93
|
+
* same name, recording the skip as a `DuplicateComposedName`. `base`'s own
|
|
94
|
+
* entries are not part of this check: a pack's composed name overriding an
|
|
95
|
+
* unrelated `base` entry is ordinary last-wins behavior, the same as
|
|
96
|
+
* `installPacks` already gives it.
|
|
97
|
+
*/
|
|
98
|
+
function mergePacksKeepingFirstClaim(packs, base, engine) {
|
|
99
|
+
const merged = { ...base };
|
|
100
|
+
const owner = new Map();
|
|
101
|
+
const duplicateComposedNames = [];
|
|
102
|
+
const droppedEngines = [];
|
|
103
|
+
for (const pack of packs) {
|
|
104
|
+
const { registry: loaded, dropped } = engine.loadPack(pack.manifest, pack.componentModules);
|
|
105
|
+
if (dropped)
|
|
106
|
+
droppedEngines.push(dropped);
|
|
107
|
+
for (const composedName of Object.keys(loaded)) {
|
|
108
|
+
if (!Object.hasOwn(loaded, composedName))
|
|
109
|
+
continue;
|
|
110
|
+
const existingOwner = owner.get(composedName);
|
|
111
|
+
if (existingOwner !== undefined) {
|
|
112
|
+
duplicateComposedNames.push({
|
|
113
|
+
composedName,
|
|
114
|
+
keptPack: existingOwner,
|
|
115
|
+
skippedPack: pack.manifest.name,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
owner.set(composedName, pack.manifest.name);
|
|
120
|
+
merged[composedName] = loaded[composedName];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
registry: engine.createRegistry(merged),
|
|
125
|
+
duplicateComposedNames,
|
|
126
|
+
droppedEngines,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Builds the render registry: `defaultRegistry` merged with every
|
|
131
|
+
* successfully validated, non-colliding pack registration in `queued`.
|
|
132
|
+
* Never throws:
|
|
133
|
+
*
|
|
134
|
+
* - an individual malformed registration is dropped (reason recorded, never
|
|
135
|
+
* shown on the page);
|
|
136
|
+
* - a namespace collision between two loaded packs (the same pack name
|
|
137
|
+
* registered twice) rejects the WHOLE install (matching docs/packs.md's
|
|
138
|
+
* install-time rejection rule) and this function falls back to
|
|
139
|
+
* `defaultRegistry` alone;
|
|
140
|
+
* - a composed-directive-name collision between two DIFFERENTLY named packs
|
|
141
|
+
* (see `DuplicateComposedName`) drops only the later claimant's
|
|
142
|
+
* component, not the whole pack.
|
|
143
|
+
*/
|
|
144
|
+
export function buildRenderRegistry(queued, defaultRegistry, engine) {
|
|
145
|
+
const packs = [];
|
|
146
|
+
const invalidReasons = [];
|
|
147
|
+
queued.forEach((entry, index) => {
|
|
148
|
+
const { pack, invalidReason } = toPackToInstall(entry, index);
|
|
149
|
+
if (pack)
|
|
150
|
+
packs.push(pack);
|
|
151
|
+
if (invalidReason)
|
|
152
|
+
invalidReasons.push(invalidReason);
|
|
153
|
+
});
|
|
154
|
+
if (packs.length === 0) {
|
|
155
|
+
return {
|
|
156
|
+
registry: defaultRegistry,
|
|
157
|
+
invalidReasons,
|
|
158
|
+
collisions: [],
|
|
159
|
+
duplicateComposedNames: [],
|
|
160
|
+
droppedEngines: [],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const namespaces = packs.map((pack) => pack.manifest.name);
|
|
164
|
+
const namespaceCollisions = detectNamespaceCollisions(namespaces);
|
|
165
|
+
if (namespaceCollisions.length > 0) {
|
|
166
|
+
return {
|
|
167
|
+
registry: engine.mergeRegistries(engine.createRegistry(), defaultRegistry),
|
|
168
|
+
invalidReasons,
|
|
169
|
+
collisions: namespaceCollisions.map((collision) => collision.namespace),
|
|
170
|
+
duplicateComposedNames: [],
|
|
171
|
+
droppedEngines: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const { registry, duplicateComposedNames, droppedEngines } = mergePacksKeepingFirstClaim(packs, defaultRegistry, engine);
|
|
175
|
+
return {
|
|
176
|
+
registry,
|
|
177
|
+
invalidReasons,
|
|
178
|
+
collisions: [],
|
|
179
|
+
duplicateComposedNames,
|
|
180
|
+
droppedEngines,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DiscoveredPack } from './discover.js';
|
|
2
|
+
import type { PackModulesMap } from '../run/lua-resolver.js';
|
|
3
|
+
export interface PackScriptsReader {
|
|
4
|
+
/** Lists one directory's immediate entries, or `[]` if it does not exist / cannot be read (never throws — a pack with no `scripts/` folder is the ordinary case). */
|
|
5
|
+
readDirectory(absoluteDir: string): Promise<ReadonlyArray<{
|
|
6
|
+
name: string;
|
|
7
|
+
isDirectory: boolean;
|
|
8
|
+
}>>;
|
|
9
|
+
/** Reads one file's UTF-8 text, or `undefined` if it cannot be read. */
|
|
10
|
+
readFile(absolutePath: string): Promise<string | undefined>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds the full `PackModulesMap` for `packs`: every discovered pack's
|
|
14
|
+
* namespace mapped to its `scripts/*.lua` files, keyed by their
|
|
15
|
+
* jail-normalized relative path. A pack with no `scripts/` directory (or an
|
|
16
|
+
* unreadable one) simply contributes an empty module map — a
|
|
17
|
+
* `require "thatPack/anything"` then resolves as an ordinary "no such
|
|
18
|
+
* module", never a crash.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadPackModules(packs: readonly DiscoveredPack[], reader: PackScriptsReader): Promise<PackModulesMap>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side, UI-free loader that reads every pack's `scripts/*.lua` files
|
|
3
|
+
* ahead of a run, producing the `PackModulesMap` `./run/lua-resolver.ts`'s
|
|
4
|
+
* `createPackModuleResolver` looks up synchronously inside the Run worker —
|
|
5
|
+
* see that module's doc comment for why the reading happens here, on the
|
|
6
|
+
* host, rather than inside the worker.
|
|
7
|
+
*
|
|
8
|
+
* Filesystem access is injected (`PackScriptsReader`) so this module is
|
|
9
|
+
* testable with an in-memory fake; `createNodeReader` in `../node-fs.ts`
|
|
10
|
+
* (exported from `@markii/host/node`) is the real `node:fs/promises`-backed
|
|
11
|
+
* implementation a Node host's pack-loading composition wires up.
|
|
12
|
+
*/
|
|
13
|
+
import { joinPath } from '../path-util.js';
|
|
14
|
+
import { normalizeBundlePath } from '@markii/bundle';
|
|
15
|
+
/** A sane upper bound on how many `.lua` files one pack's `scripts/` tree may contribute — defense in depth against a pathological pack folder (a symlink cycle a directory walk would otherwise never terminate on, or simply an absurd number of files) turning "load the installed packs" into an unbounded scan. */
|
|
16
|
+
const MAX_SCRIPT_FILES_PER_PACK = 500;
|
|
17
|
+
/** Recursion depth cap for the same reason — a plain bound alongside the file-count cap, independent of it. */
|
|
18
|
+
const MAX_SCRIPT_DIR_DEPTH = 16;
|
|
19
|
+
/**
|
|
20
|
+
* A per-file size cap on a pack's `scripts/*.lua` (H-2, pass-3 pentest report
|
|
21
|
+
* section 10.2). Packs are user-installed, trusted folders, so this is
|
|
22
|
+
* defense in depth rather than a trust boundary — but it matches the posture
|
|
23
|
+
* `buildBundleSnapshot` already takes for bundle files: a single file's whole
|
|
24
|
+
* text is read into the host process and then structured-cloned into the
|
|
25
|
+
* Run worker's job, so an absurdly large `.lua` (accidental or hostile) would
|
|
26
|
+
* otherwise pull an unbounded amount into host memory. A file over this cap
|
|
27
|
+
* is skipped exactly like an unreadable one — `require`-ing it then resolves
|
|
28
|
+
* as an ordinary "no such module", never a crash. 1 MB is far above any real
|
|
29
|
+
* shared Lua module while still bounding a pathological one.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_SCRIPT_FILE_BYTES = 1_000_000;
|
|
32
|
+
/**
|
|
33
|
+
* Walks `pack.scriptsDir` (relative to it) collecting every `.lua` file's
|
|
34
|
+
* bundle-jail-normalized relative path -> source text. A relative path the
|
|
35
|
+
* jail rejects (should not arise from a real directory listing, but this is
|
|
36
|
+
* defense in depth, matching every other filesystem-adjacent module in this
|
|
37
|
+
* codebase) is silently skipped rather than throwing — same cleanliness
|
|
38
|
+
* posture as `./discover.ts`. An unreadable individual file is skipped the
|
|
39
|
+
* same way.
|
|
40
|
+
*/
|
|
41
|
+
async function collectPackModules(pack, reader) {
|
|
42
|
+
const result = Object.create(null);
|
|
43
|
+
let fileCount = 0;
|
|
44
|
+
async function walk(relativeDir, depth) {
|
|
45
|
+
if (depth > MAX_SCRIPT_DIR_DEPTH)
|
|
46
|
+
return;
|
|
47
|
+
const absoluteDir = relativeDir
|
|
48
|
+
? joinPath(pack.scriptsDir, relativeDir)
|
|
49
|
+
: pack.scriptsDir;
|
|
50
|
+
const entries = await reader.readDirectory(absoluteDir);
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (fileCount >= MAX_SCRIPT_FILES_PER_PACK)
|
|
53
|
+
return;
|
|
54
|
+
const relativePath = relativeDir
|
|
55
|
+
? `${relativeDir}/${entry.name}`
|
|
56
|
+
: entry.name;
|
|
57
|
+
if (entry.isDirectory) {
|
|
58
|
+
await walk(relativePath, depth + 1);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!entry.name.endsWith('.lua'))
|
|
62
|
+
continue;
|
|
63
|
+
const normalized = normalizeBundlePath(relativePath);
|
|
64
|
+
if (!normalized.ok)
|
|
65
|
+
continue;
|
|
66
|
+
const absolutePath = joinPath(pack.scriptsDir, relativePath);
|
|
67
|
+
const source = await reader.readFile(absolutePath);
|
|
68
|
+
if (source === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
// H-2: skip a file whose UTF-8 size exceeds the per-file cap, same as an
|
|
71
|
+
// unreadable one. `TextEncoder` measures the encoded bytes (what
|
|
72
|
+
// actually crosses into the worker), not the JS string's UTF-16 length.
|
|
73
|
+
if (new TextEncoder().encode(source).length > MAX_SCRIPT_FILE_BYTES)
|
|
74
|
+
continue;
|
|
75
|
+
result[normalized.path] = source;
|
|
76
|
+
fileCount += 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
await walk('', 0);
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Builds the full `PackModulesMap` for `packs`: every discovered pack's
|
|
84
|
+
* namespace mapped to its `scripts/*.lua` files, keyed by their
|
|
85
|
+
* jail-normalized relative path. A pack with no `scripts/` directory (or an
|
|
86
|
+
* unreadable one) simply contributes an empty module map — a
|
|
87
|
+
* `require "thatPack/anything"` then resolves as an ordinary "no such
|
|
88
|
+
* module", never a crash.
|
|
89
|
+
*/
|
|
90
|
+
export async function loadPackModules(packs, reader) {
|
|
91
|
+
const map = Object.create(null);
|
|
92
|
+
for (const pack of packs) {
|
|
93
|
+
map[pack.manifest.name] = await collectPackModules(pack, reader);
|
|
94
|
+
}
|
|
95
|
+
return map;
|
|
96
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DEVICE-LOCAL pack trust list: which installed-pack namespaces this
|
|
3
|
+
* device authorizes to load (docs/packs.md). Every namespace named here
|
|
4
|
+
* has its pack code evaluated at host trust, so the list authorizes code
|
|
5
|
+
* execution and must live in device-local state (the adapter's
|
|
6
|
+
* `memento`), never in anything that travels with a vault, a workspace
|
|
7
|
+
* file, or a sync service.
|
|
8
|
+
*
|
|
9
|
+
* A pack folder can sit in the install root without being trusted: it
|
|
10
|
+
* arrived through a file sync, or a user copied it there by hand. Such a
|
|
11
|
+
* folder is never loaded until its namespace is added here, exactly like
|
|
12
|
+
* a freshly installed one, and the consent prompt is asked either way.
|
|
13
|
+
* The trust list, not the presence of a folder, decides whether code
|
|
14
|
+
* runs. `../host/pack-trust-behavior.ts` enforces that for every host
|
|
15
|
+
* that declares `trust: 'device-list'`.
|
|
16
|
+
*
|
|
17
|
+
* Pure and environment-free, so the shape and its hostile-input
|
|
18
|
+
* normalization are unit-testable with no host in sight.
|
|
19
|
+
*/
|
|
20
|
+
/** The memento key this shape lives under. It is the key Obsidian has always stored this list under, so an existing device keeps its trust list. */
|
|
21
|
+
export declare const PACK_TRUST_STORAGE_KEY = "markii:installedPacks";
|
|
22
|
+
/** A sane upper bound on how many namespaces this list may hold : a real device installs a handful of packs; this only exists to bound a hostile or corrupt stored value. */
|
|
23
|
+
export declare const MAX_TRUSTED_PACKS = 200;
|
|
24
|
+
/** One namespace this device trusts, and the version it was installed at (the manifest's own `version` field, or `undefined` when the pack declares none). */
|
|
25
|
+
export interface PackTrustEntry {
|
|
26
|
+
readonly namespace: string;
|
|
27
|
+
readonly version?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface PackTrustList {
|
|
30
|
+
readonly entries: readonly PackTrustEntry[];
|
|
31
|
+
}
|
|
32
|
+
export declare const DEFAULT_PACK_TRUST_LIST: PackTrustList;
|
|
33
|
+
/**
|
|
34
|
+
* Normalizes whatever `app.loadLocalStorage(PACK_TRUST_STORAGE_KEY)` handed
|
|
35
|
+
* back into a well-formed `PackTrustList` : hostile-shape-guarded the same
|
|
36
|
+
* way every other device-local shape in this plugin is, since this is also
|
|
37
|
+
* hand-editable, foreign-version-shaped data in principle. A malformed
|
|
38
|
+
* entry is dropped rather than failing the whole read; duplicates are
|
|
39
|
+
* removed by namespace, first occurrence wins; the list is bounded by
|
|
40
|
+
* `MAX_TRUSTED_PACKS`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function normalizePackTrustList(data: unknown): PackTrustList;
|
|
43
|
+
/** Whether `namespace` is trusted on this device. */
|
|
44
|
+
export declare function isPackTrusted(list: PackTrustList, namespace: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Adds (or replaces) `namespace`'s trust entry, recording `version` when
|
|
47
|
+
* given. Used both by a fresh install and by "Enable" for a pack folder
|
|
48
|
+
* that was already present on disk. Returns the same list unchanged (never
|
|
49
|
+
* a new object) when the entry is already present with the same version,
|
|
50
|
+
* so a caller can skip a write that would change nothing.
|
|
51
|
+
*/
|
|
52
|
+
export declare function trustPack(list: PackTrustList, namespace: string, version?: string): PackTrustList;
|
|
53
|
+
/**
|
|
54
|
+
* Removes `namespace`'s trust entry. Returns the same list unchanged when
|
|
55
|
+
* it was not present, so a caller can skip a no-op write.
|
|
56
|
+
*/
|
|
57
|
+
export declare function untrustPack(list: PackTrustList, namespace: string): PackTrustList;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DEVICE-LOCAL pack trust list: which installed-pack namespaces this
|
|
3
|
+
* device authorizes to load (docs/packs.md). Every namespace named here
|
|
4
|
+
* has its pack code evaluated at host trust, so the list authorizes code
|
|
5
|
+
* execution and must live in device-local state (the adapter's
|
|
6
|
+
* `memento`), never in anything that travels with a vault, a workspace
|
|
7
|
+
* file, or a sync service.
|
|
8
|
+
*
|
|
9
|
+
* A pack folder can sit in the install root without being trusted: it
|
|
10
|
+
* arrived through a file sync, or a user copied it there by hand. Such a
|
|
11
|
+
* folder is never loaded until its namespace is added here, exactly like
|
|
12
|
+
* a freshly installed one, and the consent prompt is asked either way.
|
|
13
|
+
* The trust list, not the presence of a folder, decides whether code
|
|
14
|
+
* runs. `../host/pack-trust-behavior.ts` enforces that for every host
|
|
15
|
+
* that declares `trust: 'device-list'`.
|
|
16
|
+
*
|
|
17
|
+
* Pure and environment-free, so the shape and its hostile-input
|
|
18
|
+
* normalization are unit-testable with no host in sight.
|
|
19
|
+
*/
|
|
20
|
+
/** The memento key this shape lives under. It is the key Obsidian has always stored this list under, so an existing device keeps its trust list. */
|
|
21
|
+
export const PACK_TRUST_STORAGE_KEY = 'markii:installedPacks';
|
|
22
|
+
/** A sane upper bound on how many namespaces this list may hold : a real device installs a handful of packs; this only exists to bound a hostile or corrupt stored value. */
|
|
23
|
+
export const MAX_TRUSTED_PACKS = 200;
|
|
24
|
+
export const DEFAULT_PACK_TRUST_LIST = { entries: [] };
|
|
25
|
+
function isPlainObject(value) {
|
|
26
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
/** Normalizes one raw entry, or `undefined` when it does not have a usable `namespace`. A non-string, empty, or whitespace-only `namespace` is dropped rather than crashing the whole read; a non-string `version` is dropped (the entry survives, just without one). */
|
|
29
|
+
function normalizeEntry(raw) {
|
|
30
|
+
if (!isPlainObject(raw))
|
|
31
|
+
return undefined;
|
|
32
|
+
const namespace = raw.namespace;
|
|
33
|
+
if (typeof namespace !== 'string')
|
|
34
|
+
return undefined;
|
|
35
|
+
const trimmed = namespace.trim();
|
|
36
|
+
if (trimmed.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
const version = raw.version;
|
|
39
|
+
return typeof version === 'string' && version.trim().length > 0
|
|
40
|
+
? { namespace: trimmed, version: version.trim() }
|
|
41
|
+
: { namespace: trimmed };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Normalizes whatever `app.loadLocalStorage(PACK_TRUST_STORAGE_KEY)` handed
|
|
45
|
+
* back into a well-formed `PackTrustList` : hostile-shape-guarded the same
|
|
46
|
+
* way every other device-local shape in this plugin is, since this is also
|
|
47
|
+
* hand-editable, foreign-version-shaped data in principle. A malformed
|
|
48
|
+
* entry is dropped rather than failing the whole read; duplicates are
|
|
49
|
+
* removed by namespace, first occurrence wins; the list is bounded by
|
|
50
|
+
* `MAX_TRUSTED_PACKS`.
|
|
51
|
+
*/
|
|
52
|
+
export function normalizePackTrustList(data) {
|
|
53
|
+
if (!isPlainObject(data))
|
|
54
|
+
return { ...DEFAULT_PACK_TRUST_LIST };
|
|
55
|
+
const rawEntries = data.entries;
|
|
56
|
+
if (!Array.isArray(rawEntries))
|
|
57
|
+
return { ...DEFAULT_PACK_TRUST_LIST };
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
const entries = [];
|
|
60
|
+
for (const rawEntry of rawEntries) {
|
|
61
|
+
if (entries.length >= MAX_TRUSTED_PACKS)
|
|
62
|
+
break;
|
|
63
|
+
const entry = normalizeEntry(rawEntry);
|
|
64
|
+
if (!entry)
|
|
65
|
+
continue;
|
|
66
|
+
if (seen.has(entry.namespace))
|
|
67
|
+
continue;
|
|
68
|
+
seen.add(entry.namespace);
|
|
69
|
+
entries.push(entry);
|
|
70
|
+
}
|
|
71
|
+
return { entries };
|
|
72
|
+
}
|
|
73
|
+
/** Whether `namespace` is trusted on this device. */
|
|
74
|
+
export function isPackTrusted(list, namespace) {
|
|
75
|
+
return list.entries.some((entry) => entry.namespace === namespace);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Adds (or replaces) `namespace`'s trust entry, recording `version` when
|
|
79
|
+
* given. Used both by a fresh install and by "Enable" for a pack folder
|
|
80
|
+
* that was already present on disk. Returns the same list unchanged (never
|
|
81
|
+
* a new object) when the entry is already present with the same version,
|
|
82
|
+
* so a caller can skip a write that would change nothing.
|
|
83
|
+
*/
|
|
84
|
+
export function trustPack(list, namespace, version) {
|
|
85
|
+
const trimmed = namespace.trim();
|
|
86
|
+
if (trimmed.length === 0)
|
|
87
|
+
return list;
|
|
88
|
+
const existing = list.entries.find((entry) => entry.namespace === trimmed);
|
|
89
|
+
if (existing && existing.version === version)
|
|
90
|
+
return list;
|
|
91
|
+
const withoutExisting = list.entries.filter((entry) => entry.namespace !== trimmed);
|
|
92
|
+
const next = [
|
|
93
|
+
...withoutExisting,
|
|
94
|
+
version !== undefined
|
|
95
|
+
? { namespace: trimmed, version }
|
|
96
|
+
: { namespace: trimmed },
|
|
97
|
+
];
|
|
98
|
+
return { entries: next.slice(0, MAX_TRUSTED_PACKS) };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Removes `namespace`'s trust entry. Returns the same list unchanged when
|
|
102
|
+
* it was not present, so a caller can skip a no-op write.
|
|
103
|
+
*/
|
|
104
|
+
export function untrustPack(list, namespace) {
|
|
105
|
+
if (!list.entries.some((entry) => entry.namespace === namespace)) {
|
|
106
|
+
return list;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
entries: list.entries.filter((entry) => entry.namespace !== namespace),
|
|
110
|
+
};
|
|
111
|
+
}
|