@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.
Files changed (193) hide show
  1. package/README.md +20 -2
  2. package/dist/browser-worker.js +639 -0
  3. package/dist/browser.d.ts +61 -0
  4. package/dist/browser.js +172 -0
  5. package/dist/conformance/host/01-plain-open/expected.json +5 -0
  6. package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
  7. package/dist/conformance/host/01-plain-open/scenario.json +6 -0
  8. package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
  9. package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
  10. package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
  11. package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
  12. package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
  13. package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
  14. package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
  15. package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
  16. package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
  17. package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
  18. package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
  19. package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
  20. package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
  21. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
  22. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
  23. package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
  24. package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
  25. package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
  26. package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
  27. package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
  28. package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
  29. package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
  30. package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
  31. package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
  32. package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
  33. package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
  34. package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
  35. package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
  36. package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
  37. package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
  38. package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
  39. package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
  40. package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
  41. package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
  42. package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
  43. package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
  44. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
  45. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
  46. package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
  47. package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
  48. package/dist/conformance/host/14-remove-pack/expected.json +14 -0
  49. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
  50. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
  51. package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
  52. package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
  53. package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
  54. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
  55. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
  56. package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
  57. package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
  58. package/dist/conformance/host/16-pack-trust/expected.json +37 -0
  59. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
  60. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
  61. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
  62. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
  63. package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
  64. package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
  65. package/dist/conformance/host/README.md +221 -0
  66. package/dist/conformance.d.ts +2 -0
  67. package/dist/conformance.js +5 -0
  68. package/dist/diagnostics/render-diagnostics.d.ts +41 -0
  69. package/dist/diagnostics/render-diagnostics.js +46 -0
  70. package/dist/export/cascade.d.ts +98 -0
  71. package/dist/export/cascade.js +159 -0
  72. package/dist/export/export-zip.d.ts +19 -0
  73. package/dist/export/export-zip.js +38 -0
  74. package/dist/export/image-embed.d.ts +126 -0
  75. package/dist/export/image-embed.js +274 -0
  76. package/dist/export/note-export.d.ts +332 -0
  77. package/dist/export/note-export.js +366 -0
  78. package/dist/export/note-links.d.ts +83 -0
  79. package/dist/export/note-links.js +241 -0
  80. package/dist/glue.wasm +0 -0
  81. package/dist/host/adapter.d.ts +218 -0
  82. package/dist/host/adapter.js +9 -0
  83. package/dist/host/cascade-export-behavior.d.ts +120 -0
  84. package/dist/host/cascade-export-behavior.js +151 -0
  85. package/dist/host/conformance-runner.d.ts +159 -0
  86. package/dist/host/conformance-runner.js +632 -0
  87. package/dist/host/create-host.d.ts +251 -0
  88. package/dist/host/create-host.js +329 -0
  89. package/dist/host/editor-behavior.d.ts +153 -0
  90. package/dist/host/editor-behavior.js +223 -0
  91. package/dist/host/export/md-plain.d.ts +15 -0
  92. package/dist/host/export/md-plain.js +125 -0
  93. package/dist/host/export-behavior.d.ts +111 -0
  94. package/dist/host/export-behavior.js +257 -0
  95. package/dist/host/labels.d.ts +23 -0
  96. package/dist/host/labels.js +32 -0
  97. package/dist/host/pack-archive.d.ts +30 -0
  98. package/dist/host/pack-archive.js +62 -0
  99. package/dist/host/pack-install.d.ts +87 -0
  100. package/dist/host/pack-install.js +186 -0
  101. package/dist/host/pack-load.d.ts +71 -0
  102. package/dist/host/pack-load.js +57 -0
  103. package/dist/host/pack-remove.d.ts +21 -0
  104. package/dist/host/pack-remove.js +65 -0
  105. package/dist/host/pack-trust-behavior.d.ts +79 -0
  106. package/dist/host/pack-trust-behavior.js +262 -0
  107. package/dist/host/refresh-interval.d.ts +37 -0
  108. package/dist/host/refresh-interval.js +59 -0
  109. package/dist/host/run-behavior.d.ts +88 -0
  110. package/dist/host/run-behavior.js +189 -0
  111. package/dist/host/run-diagnostics.d.ts +26 -0
  112. package/dist/host/run-diagnostics.js +43 -0
  113. package/dist/host/script-execution.d.ts +72 -0
  114. package/dist/host/script-execution.js +68 -0
  115. package/dist/host/terminal-packs.d.ts +39 -0
  116. package/dist/host/terminal-packs.js +51 -0
  117. package/dist/html-engine.d.ts +2 -0
  118. package/dist/html-engine.js +29 -0
  119. package/dist/index.d.ts +95 -0
  120. package/dist/index.js +69 -0
  121. package/dist/insert/component-catalog.d.ts +23 -0
  122. package/dist/insert/component-catalog.js +96 -0
  123. package/dist/node-fs.d.ts +14 -0
  124. package/dist/node-fs.js +70 -0
  125. package/dist/node-worker.js +20657 -0
  126. package/dist/node.d.ts +6 -0
  127. package/dist/node.js +13 -0
  128. package/dist/package.json +3 -0
  129. package/dist/packs/discover.d.ts +119 -0
  130. package/dist/packs/discover.js +266 -0
  131. package/dist/packs/pack-diagnostics.d.ts +79 -0
  132. package/dist/packs/pack-diagnostics.js +103 -0
  133. package/dist/packs/pack-paths.d.ts +53 -0
  134. package/dist/packs/pack-paths.js +105 -0
  135. package/dist/packs/pack-render-registry.d.ts +82 -0
  136. package/dist/packs/pack-render-registry.js +182 -0
  137. package/dist/packs/pack-scripts.d.ts +20 -0
  138. package/dist/packs/pack-scripts.js +96 -0
  139. package/dist/packs/pack-trust.d.ts +57 -0
  140. package/dist/packs/pack-trust.js +111 -0
  141. package/dist/packs/prebuilt.d.ts +49 -0
  142. package/dist/packs/prebuilt.js +97 -0
  143. package/dist/path-util.d.ts +24 -0
  144. package/dist/path-util.js +82 -0
  145. package/dist/run/browser-isolate.d.ts +45 -0
  146. package/dist/run/browser-isolate.js +85 -0
  147. package/dist/run/bundle-document.d.ts +29 -0
  148. package/dist/run/bundle-document.js +40 -0
  149. package/dist/run/bundle-run.d.ts +188 -0
  150. package/dist/run/bundle-run.js +232 -0
  151. package/dist/run/grant-flow.d.ts +232 -0
  152. package/dist/run/grant-flow.js +390 -0
  153. package/dist/run/ip-address.d.ts +41 -0
  154. package/dist/run/ip-address.js +255 -0
  155. package/dist/run/isolate.d.ts +59 -0
  156. package/dist/run/isolate.js +22 -0
  157. package/dist/run/lua-resolver.d.ts +21 -0
  158. package/dist/run/lua-resolver.js +63 -0
  159. package/dist/run/net-bridge-worker.d.ts +26 -0
  160. package/dist/run/net-bridge-worker.js +66 -0
  161. package/dist/run/net-bridge.d.ts +56 -0
  162. package/dist/run/net-bridge.js +59 -0
  163. package/dist/run/net-pinning.d.ts +82 -0
  164. package/dist/run/net-pinning.js +137 -0
  165. package/dist/run/net-provider.d.ts +72 -0
  166. package/dist/run/net-provider.js +303 -0
  167. package/dist/run/node-isolate.d.ts +22 -0
  168. package/dist/run/node-isolate.js +68 -0
  169. package/dist/run/run-flow.d.ts +186 -0
  170. package/dist/run/run-flow.js +323 -0
  171. package/dist/run/run-host.d.ts +107 -0
  172. package/dist/run/run-host.js +241 -0
  173. package/dist/run/run-job.d.ts +130 -0
  174. package/dist/run/run-job.js +279 -0
  175. package/dist/run/run-progress.d.ts +55 -0
  176. package/dist/run/run-progress.js +42 -0
  177. package/dist/run/run-trace.d.ts +44 -0
  178. package/dist/run/run-trace.js +36 -0
  179. package/dist/run/script-requirements.d.ts +98 -0
  180. package/dist/run/script-requirements.js +212 -0
  181. package/dist/run/snapshot-storage.d.ts +25 -0
  182. package/dist/run/snapshot-storage.js +31 -0
  183. package/dist/run/stale-values.d.ts +25 -0
  184. package/dist/run/stale-values.js +32 -0
  185. package/dist/run/worker-entry-browser.d.ts +62 -0
  186. package/dist/run/worker-entry-browser.js +172 -0
  187. package/dist/run/worker-entry.d.ts +1 -0
  188. package/dist/run/worker-entry.js +98 -0
  189. package/dist/values-failure.d.ts +17 -0
  190. package/dist/values-failure.js +1 -0
  191. package/dist/values-merge.d.ts +39 -0
  192. package/dist/values-merge.js +33 -0
  193. 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
+ }