@immediately-run/sdk 0.45.3 → 0.47.0

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 (219) hide show
  1. package/dist/MDXProvider.cjs +1 -5
  2. package/dist/MDXProvider.cjs.map +1 -1
  3. package/dist/MDXProvider.js +1 -5
  4. package/dist/MDXProvider.js.map +1 -1
  5. package/dist/RoutingSpec.cjs.map +1 -1
  6. package/dist/TinkerableContext.cjs.map +1 -1
  7. package/dist/TinkerableContext.js.map +1 -1
  8. package/dist/auth.cjs.map +1 -1
  9. package/dist/auth.js.map +1 -1
  10. package/dist/boot.cjs +2 -9
  11. package/dist/boot.cjs.map +1 -1
  12. package/dist/boot.d.cts +2 -2
  13. package/dist/boot.d.ts +2 -2
  14. package/dist/boot.js +2 -9
  15. package/dist/boot.js.map +1 -1
  16. package/dist/catalog.cjs.map +1 -1
  17. package/dist/catalog.js.map +1 -1
  18. package/dist/components/Admonition.cjs +4 -13
  19. package/dist/components/Admonition.cjs.map +1 -1
  20. package/dist/components/Admonition.js +4 -13
  21. package/dist/components/Admonition.js.map +1 -1
  22. package/dist/components/FileRouter.cjs +11 -3
  23. package/dist/components/FileRouter.cjs.map +1 -1
  24. package/dist/components/FileRouter.js +11 -3
  25. package/dist/components/FileRouter.js.map +1 -1
  26. package/dist/components/HeadingAnchor.cjs.map +1 -1
  27. package/dist/components/HeadingAnchor.js.map +1 -1
  28. package/dist/components/Link.cjs.map +1 -1
  29. package/dist/components/Link.js.map +1 -1
  30. package/dist/components/MainContent.cjs +11 -1
  31. package/dist/components/MainContent.cjs.map +1 -1
  32. package/dist/components/MainContent.js +11 -1
  33. package/dist/components/MainContent.js.map +1 -1
  34. package/dist/components/MountImage.cjs +1 -9
  35. package/dist/components/MountImage.cjs.map +1 -1
  36. package/dist/components/MountImage.js +1 -9
  37. package/dist/components/MountImage.js.map +1 -1
  38. package/dist/components/Routes.cjs +1 -4
  39. package/dist/components/Routes.cjs.map +1 -1
  40. package/dist/components/Routes.d.cts +1 -1
  41. package/dist/components/Routes.d.ts +1 -1
  42. package/dist/components/Routes.js +1 -4
  43. package/dist/components/Routes.js.map +1 -1
  44. package/dist/components/SafeInclude.cjs +1 -4
  45. package/dist/components/SafeInclude.cjs.map +1 -1
  46. package/dist/components/SafeInclude.js +1 -4
  47. package/dist/components/SafeInclude.js.map +1 -1
  48. package/dist/components/WikiLink.cjs +1 -10
  49. package/dist/components/WikiLink.cjs.map +1 -1
  50. package/dist/components/WikiLink.js +1 -10
  51. package/dist/components/WikiLink.js.map +1 -1
  52. package/dist/components/defaults.cjs.map +1 -1
  53. package/dist/components/defaults.d.cts +1 -1
  54. package/dist/components/defaults.d.ts +1 -1
  55. package/dist/components/defaults.js.map +1 -1
  56. package/dist/components/errors.cjs +3 -1
  57. package/dist/components/errors.cjs.map +1 -1
  58. package/dist/components/errors.js +3 -1
  59. package/dist/components/errors.js.map +1 -1
  60. package/dist/contextUtils.cjs.map +1 -1
  61. package/dist/contextUtils.js.map +1 -1
  62. package/dist/contribute.cjs.map +1 -1
  63. package/dist/contribute.js.map +1 -1
  64. package/dist/debug.cjs +2 -1
  65. package/dist/debug.cjs.map +1 -1
  66. package/dist/debug.js +3 -8
  67. package/dist/debug.js.map +1 -1
  68. package/dist/diagnostics.cjs.map +1 -1
  69. package/dist/diagnostics.js.map +1 -1
  70. package/dist/editor.cjs.map +1 -1
  71. package/dist/editor.js.map +1 -1
  72. package/dist/editorContext.cjs.map +1 -1
  73. package/dist/editorContext.js.map +1 -1
  74. package/dist/fs.cjs.map +1 -1
  75. package/dist/fs.js.map +1 -1
  76. package/dist/hostAttention.cjs +57 -0
  77. package/dist/hostAttention.cjs.map +1 -0
  78. package/dist/hostAttention.d.cts +39 -0
  79. package/dist/hostAttention.d.ts +39 -0
  80. package/dist/hostAttention.js +31 -0
  81. package/dist/hostAttention.js.map +1 -0
  82. package/dist/hostTransport.cjs +55 -0
  83. package/dist/hostTransport.cjs.map +1 -0
  84. package/dist/hostTransport.d.cts +12 -0
  85. package/dist/hostTransport.d.ts +12 -0
  86. package/dist/hostTransport.js +30 -0
  87. package/dist/hostTransport.js.map +1 -0
  88. package/dist/index.cjs +4 -0
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +3 -1
  91. package/dist/index.d.ts +3 -1
  92. package/dist/index.js +2 -0
  93. package/dist/index.js.map +1 -1
  94. package/dist/injectedBundler.cjs.map +1 -1
  95. package/dist/injectedBundler.js.map +1 -1
  96. package/dist/ipc.cjs +1 -4
  97. package/dist/ipc.cjs.map +1 -1
  98. package/dist/ipc.js +1 -4
  99. package/dist/ipc.js.map +1 -1
  100. package/dist/irMarkers.cjs.map +1 -1
  101. package/dist/irMarkers.d.cts +11 -11
  102. package/dist/irMarkers.d.ts +11 -11
  103. package/dist/irMarkers.js.map +1 -1
  104. package/dist/launch.cjs.map +1 -1
  105. package/dist/launch.js.map +1 -1
  106. package/dist/linkSpace.cjs +11 -2
  107. package/dist/linkSpace.cjs.map +1 -1
  108. package/dist/linkSpace.d.cts +23 -0
  109. package/dist/linkSpace.d.ts +23 -0
  110. package/dist/linkSpace.js +11 -2
  111. package/dist/linkSpace.js.map +1 -1
  112. package/dist/llm.cjs +26 -8
  113. package/dist/llm.cjs.map +1 -1
  114. package/dist/llm.d.cts +75 -3
  115. package/dist/llm.d.ts +75 -3
  116. package/dist/llm.js +21 -7
  117. package/dist/llm.js.map +1 -1
  118. package/dist/loading.cjs +18 -14
  119. package/dist/loading.cjs.map +1 -1
  120. package/dist/loading.d.cts +2 -2
  121. package/dist/loading.d.ts +2 -2
  122. package/dist/loading.js +19 -19
  123. package/dist/loading.js.map +1 -1
  124. package/dist/markers.cjs.map +1 -1
  125. package/dist/markers.js.map +1 -1
  126. package/dist/metadataSource.cjs +1 -5
  127. package/dist/metadataSource.cjs.map +1 -1
  128. package/dist/metadataSource.d.cts +1 -1
  129. package/dist/metadataSource.d.ts +1 -1
  130. package/dist/metadataSource.js +1 -5
  131. package/dist/metadataSource.js.map +1 -1
  132. package/dist/moduleCache.cjs +4 -1
  133. package/dist/moduleCache.cjs.map +1 -1
  134. package/dist/moduleCache.d.cts +1 -1
  135. package/dist/moduleCache.d.ts +1 -1
  136. package/dist/moduleCache.js +4 -1
  137. package/dist/moduleCache.js.map +1 -1
  138. package/dist/mountMatch.cjs.map +1 -1
  139. package/dist/mountMatch.js.map +1 -1
  140. package/dist/mounts.cjs +6 -1
  141. package/dist/mounts.cjs.map +1 -1
  142. package/dist/mounts.d.cts +2 -2
  143. package/dist/mounts.d.ts +2 -2
  144. package/dist/mounts.js +6 -1
  145. package/dist/mounts.js.map +1 -1
  146. package/dist/netFetch.cjs +3 -5
  147. package/dist/netFetch.cjs.map +1 -1
  148. package/dist/netFetch.js +3 -5
  149. package/dist/netFetch.js.map +1 -1
  150. package/dist/onFsChange.cjs.map +1 -1
  151. package/dist/onFsChange.js.map +1 -1
  152. package/dist/pathUtils.cjs +9 -12
  153. package/dist/pathUtils.cjs.map +1 -1
  154. package/dist/pathUtils.js +9 -12
  155. package/dist/pathUtils.js.map +1 -1
  156. package/dist/protocolDeadline.cjs +205 -0
  157. package/dist/protocolDeadline.cjs.map +1 -0
  158. package/dist/protocolDeadline.d.cts +146 -0
  159. package/dist/protocolDeadline.d.ts +146 -0
  160. package/dist/protocolDeadline.js +168 -0
  161. package/dist/protocolDeadline.js.map +1 -0
  162. package/dist/protocolStream.cjs +49 -6
  163. package/dist/protocolStream.cjs.map +1 -1
  164. package/dist/protocolStream.d.cts +9 -2
  165. package/dist/protocolStream.d.ts +9 -2
  166. package/dist/protocolStream.js +56 -6
  167. package/dist/protocolStream.js.map +1 -1
  168. package/dist/pushChannel.cjs +13 -9
  169. package/dist/pushChannel.cjs.map +1 -1
  170. package/dist/pushChannel.js +12 -8
  171. package/dist/pushChannel.js.map +1 -1
  172. package/dist/ready.cjs.map +1 -1
  173. package/dist/ready.js.map +1 -1
  174. package/dist/routing.cjs +3 -1
  175. package/dist/routing.cjs.map +1 -1
  176. package/dist/routing.js +3 -1
  177. package/dist/routing.js.map +1 -1
  178. package/dist/safeContent/index.cjs.map +1 -1
  179. package/dist/safeContent/index.js.map +1 -1
  180. package/dist/safeContent/parseSafeMdast.cjs.map +1 -1
  181. package/dist/safeContent/parseSafeMdast.js.map +1 -1
  182. package/dist/safeContent/renderMdast.cjs.map +1 -1
  183. package/dist/safeContent/renderMdast.js.map +1 -1
  184. package/dist/sandboxTypes.cjs.map +1 -1
  185. package/dist/sandboxUtils.cjs +69 -24
  186. package/dist/sandboxUtils.cjs.map +1 -1
  187. package/dist/sandboxUtils.d.cts +37 -4
  188. package/dist/sandboxUtils.d.ts +37 -4
  189. package/dist/sandboxUtils.js +74 -22
  190. package/dist/sandboxUtils.js.map +1 -1
  191. package/dist/scrollToId.cjs.map +1 -1
  192. package/dist/scrollToId.js.map +1 -1
  193. package/dist/secrets.cjs.map +1 -1
  194. package/dist/secrets.js.map +1 -1
  195. package/dist/tasks.cjs +12 -2
  196. package/dist/tasks.cjs.map +1 -1
  197. package/dist/tasks.js +12 -2
  198. package/dist/tasks.js.map +1 -1
  199. package/dist/testing.cjs +1 -3
  200. package/dist/testing.cjs.map +1 -1
  201. package/dist/testing.js +1 -3
  202. package/dist/testing.js.map +1 -1
  203. package/dist/theme.cjs.map +1 -1
  204. package/dist/theme.js.map +1 -1
  205. package/dist/urlUtils.cjs +7 -18
  206. package/dist/urlUtils.cjs.map +1 -1
  207. package/dist/urlUtils.js +7 -18
  208. package/dist/urlUtils.js.map +1 -1
  209. package/dist/vcs.cjs +1 -3
  210. package/dist/vcs.cjs.map +1 -1
  211. package/dist/vcs.js +1 -3
  212. package/dist/vcs.js.map +1 -1
  213. package/dist/version.cjs +1 -1
  214. package/dist/version.cjs.map +1 -1
  215. package/dist/version.d.cts +1 -1
  216. package/dist/version.d.ts +1 -1
  217. package/dist/version.js +1 -1
  218. package/dist/version.js.map +1 -1
  219. package/package.json +11 -5
package/dist/tasks.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tasks.ts"],"sourcesContent":["// Task invocation — apps invoking apps (UI_AS_APPS_SPEC §5.7). The\n// `startActivityForResult` pattern: one app invokes another by TASK CONTRACT\n// (never by app name — the user's override picks the bound app), passes typed\n// params, and awaits a typed result. The callee runs in a host-owned overlay\n// under ITS OWN grants — data crosses, your authority does not (§5.7).\n//\n// Two roles:\n// - CALLER: `invokeTask(task, params)` (Recipe B — a deferred reply the host\n// holds open until the callee finishes). Delegate a file with `capFile(...)`:\n// the host resolves it against YOUR grants and mints an attenuated chroot.\n// - CALLEE: read `useTaskInput()`, then `completeTask(result)` / `cancelTask()`.\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { PROTOCOL_TASK, TASK_CANCEL, TASK_COMPLETE, TASK_INPUT } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n// ── caller side ─────────────────────────────────────────────────────────────\n\n/** A delegated FILE capability marker for a task param (§5.7). */\nexport interface FileCap {\n $cap: 'file';\n mountId: string;\n relPath: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Build a delegated file reference for a task param. The host resolves it against\n * YOUR OWN grants and mints an attenuated, task-scoped chroot for the callee — you\n * can only delegate a path you already hold (attenuation only, never escalation).\n *\n * file: capFile({ mountId: 'space:abc', relPath: 'photos/cat.jpg' }, { mode: 'rw' })\n */\nexport const capFile = (\n ref: { mountId: string; relPath: string },\n opts: { mode: 'ro' | 'rw' },\n): FileCap => ({ $cap: 'file', mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });\n\n/** A delegated DIRECTORY capability marker for a task param (D2). Like {@link FileCap}\n * but `relPath` names a DIRECTORY: the host chroots the callee AT that directory\n * (the whole subtree). Used for the `pick-file` `roots` — one chroot per root. */\nexport interface DirCap {\n $cap: 'dir';\n mountId: string;\n relPath: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Build a delegated DIRECTORY reference for a task param (the directory analogue of\n * {@link capFile}). The host resolves it against YOUR OWN grants and mints an\n * attenuated, task-scoped chroot of that directory for the callee — you can only\n * delegate a directory you already hold (attenuation only, never escalation):\n *\n * roots: [capDir({ mountId: 'space:abc', relPath: 'boards' }, { mode: 'rw' })]\n */\nexport const capDir = (\n ref: { mountId: string; relPath: string },\n opts: { mode: 'ro' | 'rw' },\n): DirCap => ({ $cap: 'dir', mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });\n\n/**\n * Invoke another app via a task contract and await its typed result (Recipe B).\n * Rejects with a machine `.code` on refusal: `cancelled` (user dismissed the\n * overlay), `timeout` (§5.7.1 liveness), `forbidden` (undeclared task or a file\n * delegation you don't hold), `no-such-task`, `task-cycle`/`task-depth-exceeded`/\n * `task-version-mismatch`, or `invalid-params` (result failed the contract schema).\n */\nexport const invokeTask = async <R = unknown>(\n task: string,\n params: Record<string, unknown> = {},\n): Promise<R> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_TASK], 'invoke', [{ task, params }])) as\n | { ok: true; data: R }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `task '${task}' failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data;\n};\n\n// ── callee side ─────────────────────────────────────────────────────────────\n\n/** The params this app was invoked with as a task callee. */\nexport interface TaskInput {\n task: string;\n params: Record<string, unknown>;\n}\n\nlet latestInput: TaskInput | null = null;\nconst inputListeners = new Set<(i: TaskInput) => void>();\n\n// The host delivers a `task-input` message to the callee's iframe right after it\n// mounts the overlay (the §5.7 \"params via the region's mount event\").\naddListener(TASK_INPUT, (m: { task: string; params?: Record<string, unknown> }) => {\n latestInput = { task: m.task, params: m.params ?? {} };\n inputListeners.forEach((l) => l(latestInput!));\n});\n\n/** The task params this app was invoked with, or null if it isn't a task callee. */\nexport const getTaskInput = (): TaskInput | null => latestInput;\n\n/**\n * Finish the task, returning a result to the caller. The host validates it against\n * the contract's result schema before resolving the caller (`invalid-params` on\n * violation), then tears down this overlay.\n */\nexport const completeTask = (result: unknown): void => sendMessage(TASK_COMPLETE, { result });\n\n/** Abort the task; the caller's `invokeTask` rejects with `cancelled`. */\nexport const cancelTask = (): void => sendMessage(TASK_CANCEL, {});\n\n/** React hook: the task input for this callee, re-rendering when it arrives. */\nexport const useTaskInput = (): TaskInput | null => {\n const [input, setInput] = useState<TaskInput | null>(getTaskInput);\n useEffect(() => {\n const l = (i: TaskInput) => setInput(i);\n inputListeners.add(l);\n if (latestInput) setInput(latestInput);\n return () => {\n inputListeners.delete(l);\n };\n }, []);\n return input;\n};\n"],"mappings":";AAWA,SAAS,WAAW,gBAAgB;AACpC,SAAS,iBAAiB,aAAa,mBAAmB;AAC1D,SAAS,eAAe,aAAa,eAAe,kBAAkB;AACtE,SAAS,eAAe;AAmBjB,MAAM,UAAU,CACrB,KACA,UACa,EAAE,MAAM,QAAQ,SAAS,IAAI,SAAS,SAAS,IAAI,SAAS,MAAM,KAAK,KAAK;AAoBpF,MAAM,SAAS,CACpB,KACA,UACY,EAAE,MAAM,OAAO,SAAS,IAAI,SAAS,SAAS,IAAI,SAAS,MAAM,KAAK,KAAK;AASlF,MAAM,aAAa,OACxB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,MAAO,MAAM,gBAAgB,QAAQ,aAAa,GAAG,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,CAAC;AAIvF,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,SAAS,IAAI,UAAU;AAC7D,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAUA,IAAI,cAAgC;AACpC,MAAM,iBAAiB,oBAAI,IAA4B;AAIvD,YAAY,YAAY,CAAC,MAA0D;AACjF,gBAAc,EAAE,MAAM,EAAE,MAAM,QAAQ,EAAE,UAAU,CAAC,EAAE;AACrD,iBAAe,QAAQ,CAAC,MAAM,EAAE,WAAY,CAAC;AAC/C,CAAC;AAGM,MAAM,eAAe,MAAwB;AAO7C,MAAM,eAAe,CAAC,WAA0B,YAAY,eAAe,EAAE,OAAO,CAAC;AAGrF,MAAM,aAAa,MAAY,YAAY,aAAa,CAAC,CAAC;AAG1D,MAAM,eAAe,MAAwB;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA2B,YAAY;AACjE,YAAU,MAAM;AACd,UAAM,IAAI,CAAC,MAAiB,SAAS,CAAC;AACtC,mBAAe,IAAI,CAAC;AACpB,QAAI,YAAa,UAAS,WAAW;AACrC,WAAO,MAAM;AACX,qBAAe,OAAO,CAAC;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,CAAC;AACL,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/tasks.ts"],"sourcesContent":["// Task invocation — apps invoking apps (UI_AS_APPS_SPEC §5.7). The\n// `startActivityForResult` pattern: one app invokes another by TASK CONTRACT\n// (never by app name — the user's override picks the bound app), passes typed\n// params, and awaits a typed result. The callee runs in a host-owned overlay\n// under ITS OWN grants — data crosses, your authority does not (§5.7).\n//\n// Two roles:\n// - CALLER: `invokeTask(task, params)` (Recipe B — a deferred reply the host\n// holds open until the callee finishes). Delegate a file with `capFile(...)`:\n// the host resolves it against YOUR grants and mints an attenuated chroot.\n// - CALLEE: read `useTaskInput()`, then `completeTask(result)` / `cancelTask()`.\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { PROTOCOL_TASK, TASK_CANCEL, TASK_COMPLETE, TASK_INPUT } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n// ── caller side ─────────────────────────────────────────────────────────────\n\n/** A delegated FILE capability marker for a task param (§5.7). */\nexport interface FileCap {\n $cap: 'file';\n mountId: string;\n relPath: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Build a delegated file reference for a task param. The host resolves it against\n * YOUR OWN grants and mints an attenuated, task-scoped chroot for the callee — you\n * can only delegate a path you already hold (attenuation only, never escalation).\n *\n * file: capFile({ mountId: 'space:abc', relPath: 'photos/cat.jpg' }, { mode: 'rw' })\n */\nexport const capFile = (ref: { mountId: string; relPath: string }, opts: { mode: 'ro' | 'rw' }): FileCap => ({\n $cap: 'file',\n mountId: ref.mountId,\n relPath: ref.relPath,\n mode: opts.mode,\n});\n\n/** A delegated DIRECTORY capability marker for a task param (D2). Like {@link FileCap}\n * but `relPath` names a DIRECTORY: the host chroots the callee AT that directory\n * (the whole subtree). Used for the `pick-file` `roots` — one chroot per root. */\nexport interface DirCap {\n $cap: 'dir';\n mountId: string;\n relPath: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Build a delegated DIRECTORY reference for a task param (the directory analogue of\n * {@link capFile}). The host resolves it against YOUR OWN grants and mints an\n * attenuated, task-scoped chroot of that directory for the callee — you can only\n * delegate a directory you already hold (attenuation only, never escalation):\n *\n * roots: [capDir({ mountId: 'space:abc', relPath: 'boards' }, { mode: 'rw' })]\n */\nexport const capDir = (ref: { mountId: string; relPath: string }, opts: { mode: 'ro' | 'rw' }): DirCap => ({\n $cap: 'dir',\n mountId: ref.mountId,\n relPath: ref.relPath,\n mode: opts.mode,\n});\n\n/**\n * Invoke another app via a task contract and await its typed result (Recipe B).\n * Rejects with a machine `.code` on refusal: `cancelled` (user dismissed the\n * overlay), `timeout` (§5.7.1 liveness), `forbidden` (undeclared task or a file\n * delegation you don't hold), `no-such-task`, `task-cycle`/`task-depth-exceeded`/\n * `task-version-mismatch`, or `invalid-params` (result failed the contract schema).\n */\nexport const invokeTask = async <R = unknown>(task: string, params: Record<string, unknown> = {}): Promise<R> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_TASK], 'invoke', [{ task, params }])) as\n | { ok: true; data: R }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `task '${task}' failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data;\n};\n\n// ── callee side ─────────────────────────────────────────────────────────────\n\n/** The params this app was invoked with as a task callee. */\nexport interface TaskInput {\n task: string;\n params: Record<string, unknown>;\n}\n\nlet latestInput: TaskInput | null = null;\nconst inputListeners = new Set<(i: TaskInput) => void>();\n\n// The host delivers a `task-input` message to the callee's iframe right after it\n// mounts the overlay (the §5.7 \"params via the region's mount event\").\naddListener(TASK_INPUT, (m: { task: string; params?: Record<string, unknown> }) => {\n latestInput = { task: m.task, params: m.params ?? {} };\n inputListeners.forEach((l) => l(latestInput!));\n});\n\n/** The task params this app was invoked with, or null if it isn't a task callee. */\nexport const getTaskInput = (): TaskInput | null => latestInput;\n\n/**\n * Finish the task, returning a result to the caller. The host validates it against\n * the contract's result schema before resolving the caller (`invalid-params` on\n * violation), then tears down this overlay.\n */\nexport const completeTask = (result: unknown): void => sendMessage(TASK_COMPLETE, { result });\n\n/** Abort the task; the caller's `invokeTask` rejects with `cancelled`. */\nexport const cancelTask = (): void => sendMessage(TASK_CANCEL, {});\n\n/** React hook: the task input for this callee, re-rendering when it arrives. */\nexport const useTaskInput = (): TaskInput | null => {\n const [input, setInput] = useState<TaskInput | null>(getTaskInput);\n useEffect(() => {\n const l = (i: TaskInput) => setInput(i);\n inputListeners.add(l);\n if (latestInput) setInput(latestInput);\n return () => {\n inputListeners.delete(l);\n };\n }, []);\n return input;\n};\n"],"mappings":";AAWA,SAAS,WAAW,gBAAgB;AACpC,SAAS,iBAAiB,aAAa,mBAAmB;AAC1D,SAAS,eAAe,aAAa,eAAe,kBAAkB;AACtE,SAAS,eAAe;AAmBjB,MAAM,UAAU,CAAC,KAA2C,UAA0C;AAAA,EAC3G,MAAM;AAAA,EACN,SAAS,IAAI;AAAA,EACb,SAAS,IAAI;AAAA,EACb,MAAM,KAAK;AACb;AAoBO,MAAM,SAAS,CAAC,KAA2C,UAAyC;AAAA,EACzG,MAAM;AAAA,EACN,SAAS,IAAI;AAAA,EACb,SAAS,IAAI;AAAA,EACb,MAAM,KAAK;AACb;AASO,MAAM,aAAa,OAAoB,MAAc,SAAkC,CAAC,MAAkB;AAC/G,QAAM,MAAO,MAAM,gBAAgB,QAAQ,aAAa,GAAG,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,CAAC;AAIvF,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,SAAS,IAAI,UAAU;AAC7D,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAUA,IAAI,cAAgC;AACpC,MAAM,iBAAiB,oBAAI,IAA4B;AAIvD,YAAY,YAAY,CAAC,MAA0D;AACjF,gBAAc,EAAE,MAAM,EAAE,MAAM,QAAQ,EAAE,UAAU,CAAC,EAAE;AACrD,iBAAe,QAAQ,CAAC,MAAM,EAAE,WAAY,CAAC;AAC/C,CAAC;AAGM,MAAM,eAAe,MAAwB;AAO7C,MAAM,eAAe,CAAC,WAA0B,YAAY,eAAe,EAAE,OAAO,CAAC;AAGrF,MAAM,aAAa,MAAY,YAAY,aAAa,CAAC,CAAC;AAG1D,MAAM,eAAe,MAAwB;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA2B,YAAY;AACjE,YAAU,MAAM;AACd,UAAM,IAAI,CAAC,MAAiB,SAAS,CAAC;AACtC,mBAAe,IAAI,CAAC;AACpB,QAAI,YAAa,UAAS,WAAW;AACrC,WAAO,MAAM;AACX,qBAAe,OAAO,CAAC;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,CAAC;AACL,SAAO;AACT;","names":[]}
package/dist/testing.cjs CHANGED
@@ -36,9 +36,7 @@ function createMockHost() {
36
36
  protocolCalls.push({ protocol, method, params });
37
37
  const responder = responders.get(key(protocol, method));
38
38
  if (!responder) {
39
- return Promise.reject(
40
- new Error(`mockHost: no stub for protocolRequest('${protocol}', '${method}')`)
41
- );
39
+ return Promise.reject(new Error(`mockHost: no stub for protocolRequest('${protocol}', '${method}')`));
42
40
  }
43
41
  return Promise.resolve().then(() => responder(params));
44
42
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing.ts"],"sourcesContent":["// Public testing utility — `@immediately-run/sdk/testing` (TESTING_AUTOMATION_SPEC\n// §3/§7). A mock host transport so apps built on this SDK can unit-test their host\n// interaction in CI, with no live bundler/host: drive the SDK's REAL transport path\n// by installing this at the §4 discovery global, then `emit()` host→app pushes and\n// assert your app reacts.\n//\n// import { createMockHost } from '@immediately-run/sdk/testing';\n// const host = createMockHost();\n// host.install(); // SDK now resolves this transport\n// host.stubProtocol('spaces', 'list', () => ({ ok: true, data: [] }));\n// // …render your app / call SDK functions…\n// host.emit({ type: 'mount-add', mount: { path: '/spaces/a', type: 'firestore', id: 'a' } });\n// expect(host.sent).toContainEqual({ type: 'request-mounts', data: {} });\n// host.uninstall();\n//\n// It implements the `sandboxUtils` host transport (sendMessage / protocolRequest /\n// onMessage), so tests exercise the actual `transport()` resolution, the\n// `addListener` type-filter, and `protocolRequest` round-trips — higher fidelity\n// than mocking `./sandboxUtils` wholesale.\n\n/** A message pushed host → app, as it arrives on `onMessage`. */\nexport interface HostMessage {\n type: string;\n [k: string]: unknown;\n}\n\n/** An outbound `sendMessage` captured by the mock. */\nexport interface SentMessage {\n type: string;\n data: Record<string, unknown>;\n}\n\n/** An outbound `protocolRequest` captured by the mock. */\nexport interface ProtocolCall {\n protocol: string;\n method: string;\n params: unknown[];\n}\n\n/** Responder stub for a `protocolRequest(protocol, method, …)`. */\nexport type ProtocolResponder = (params: unknown[]) => unknown;\n\n/** Extra §4 discovery-global fields to publish alongside `transport` on install. */\nexport interface MockHostGlobalExtras {\n runtimeVersion?: string;\n protocolVersion?: string;\n appMountPath?: string;\n}\n\ninterface ImmediatelyRunGlobal extends MockHostGlobalExtras {\n transport?: unknown;\n}\n\n/** The controllable mock host returned by {@link createMockHost}. */\nexport interface MockHost {\n /** The §4 host transport object (also published on the global by `install`). */\n transport: {\n sendMessage(type: string, data?: Record<string, unknown>): void;\n protocolRequest(protocol: string, method: string, params: unknown[]): Promise<unknown>;\n onMessage(handler: (msg: HostMessage) => void): { dispose(): void };\n };\n /** Publish the transport at `globalThis.__immediatelyRun__` (npm-fetched path). */\n install(extras?: MockHostGlobalExtras): void;\n /** Remove the discovery global (call in `afterEach`). */\n uninstall(): void;\n /** Simulate a host → app push; delivered to every live `onMessage` handler. */\n emit(msg: HostMessage): void;\n /** Register a response for a `protocolRequest`; unstubbed calls reject. */\n stubProtocol(protocol: string, method: string, responder: ProtocolResponder): void;\n /** Every `sendMessage` the SDK made, in order. */\n readonly sent: SentMessage[];\n /** Every `protocolRequest` the SDK made, in order. */\n readonly protocolCalls: ProtocolCall[];\n /** Count of live `onMessage` listeners (leak checks). */\n handlerCount(): number;\n}\n\nconst GLOBAL_KEY = '__immediatelyRun__';\n\n/** Build a controllable mock host. Nothing is global until you call `install()`. */\nexport function createMockHost(): MockHost {\n const handlers = new Set<(msg: HostMessage) => void>();\n const sent: SentMessage[] = [];\n const protocolCalls: ProtocolCall[] = [];\n const responders = new Map<string, ProtocolResponder>();\n const key = (protocol: string, method: string) => `${protocol}::${method}`;\n\n const transport: MockHost['transport'] = {\n sendMessage(type, data = {}) {\n sent.push({ type, data });\n },\n protocolRequest(protocol, method, params) {\n protocolCalls.push({ protocol, method, params });\n const responder = responders.get(key(protocol, method));\n if (!responder) {\n return Promise.reject(\n new Error(`mockHost: no stub for protocolRequest('${protocol}', '${method}')`),\n );\n }\n // Resolve async so callers observe real microtask ordering.\n return Promise.resolve().then(() => responder(params));\n },\n onMessage(handler) {\n handlers.add(handler);\n return { dispose: () => handlers.delete(handler) };\n },\n };\n\n const g = globalThis as { [GLOBAL_KEY]?: ImmediatelyRunGlobal };\n\n return {\n transport,\n install(extras = {}) {\n g[GLOBAL_KEY] = { ...extras, transport };\n },\n uninstall() {\n delete g[GLOBAL_KEY];\n },\n emit(msg) {\n // Copy so a handler that unsubscribes mid-emit doesn't mutate the live set.\n for (const handler of [...handlers]) handler(msg);\n },\n stubProtocol(protocol, method, responder) {\n responders.set(key(protocol, method), responder);\n },\n sent,\n protocolCalls,\n handlerCount: () => handlers.size,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA6EA,MAAM,aAAa;AAGZ,SAAS,iBAA2B;AACzC,QAAM,WAAW,oBAAI,IAAgC;AACrD,QAAM,OAAsB,CAAC;AAC7B,QAAM,gBAAgC,CAAC;AACvC,QAAM,aAAa,oBAAI,IAA+B;AACtD,QAAM,MAAM,CAAC,UAAkB,WAAmB,GAAG,QAAQ,KAAK,MAAM;AAExE,QAAM,YAAmC;AAAA,IACvC,YAAY,MAAM,OAAO,CAAC,GAAG;AAC3B,WAAK,KAAK,EAAE,MAAM,KAAK,CAAC;AAAA,IAC1B;AAAA,IACA,gBAAgB,UAAU,QAAQ,QAAQ;AACxC,oBAAc,KAAK,EAAE,UAAU,QAAQ,OAAO,CAAC;AAC/C,YAAM,YAAY,WAAW,IAAI,IAAI,UAAU,MAAM,CAAC;AACtD,UAAI,CAAC,WAAW;AACd,eAAO,QAAQ;AAAA,UACb,IAAI,MAAM,0CAA0C,QAAQ,OAAO,MAAM,IAAI;AAAA,QAC/E;AAAA,MACF;AAEA,aAAO,QAAQ,QAAQ,EAAE,KAAK,MAAM,UAAU,MAAM,CAAC;AAAA,IACvD;AAAA,IACA,UAAU,SAAS;AACjB,eAAS,IAAI,OAAO;AACpB,aAAO,EAAE,SAAS,MAAM,SAAS,OAAO,OAAO,EAAE;AAAA,IACnD;AAAA,EACF;AAEA,QAAM,IAAI;AAEV,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,SAAS,CAAC,GAAG;AACnB,QAAE,UAAU,IAAI,EAAE,GAAG,QAAQ,UAAU;AAAA,IACzC;AAAA,IACA,YAAY;AACV,aAAO,EAAE,UAAU;AAAA,IACrB;AAAA,IACA,KAAK,KAAK;AAER,iBAAW,WAAW,CAAC,GAAG,QAAQ,EAAG,SAAQ,GAAG;AAAA,IAClD;AAAA,IACA,aAAa,UAAU,QAAQ,WAAW;AACxC,iBAAW,IAAI,IAAI,UAAU,MAAM,GAAG,SAAS;AAAA,IACjD;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,MAAM,SAAS;AAAA,EAC/B;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/testing.ts"],"sourcesContent":["// Public testing utility — `@immediately-run/sdk/testing` (TESTING_AUTOMATION_SPEC\n// §3/§7). A mock host transport so apps built on this SDK can unit-test their host\n// interaction in CI, with no live bundler/host: drive the SDK's REAL transport path\n// by installing this at the §4 discovery global, then `emit()` host→app pushes and\n// assert your app reacts.\n//\n// import { createMockHost } from '@immediately-run/sdk/testing';\n// const host = createMockHost();\n// host.install(); // SDK now resolves this transport\n// host.stubProtocol('spaces', 'list', () => ({ ok: true, data: [] }));\n// // …render your app / call SDK functions…\n// host.emit({ type: 'mount-add', mount: { path: '/spaces/a', type: 'firestore', id: 'a' } });\n// expect(host.sent).toContainEqual({ type: 'request-mounts', data: {} });\n// host.uninstall();\n//\n// It implements the `sandboxUtils` host transport (sendMessage / protocolRequest /\n// onMessage), so tests exercise the actual `transport()` resolution, the\n// `addListener` type-filter, and `protocolRequest` round-trips — higher fidelity\n// than mocking `./sandboxUtils` wholesale.\n\n/** A message pushed host → app, as it arrives on `onMessage`. */\nexport interface HostMessage {\n type: string;\n [k: string]: unknown;\n}\n\n/** An outbound `sendMessage` captured by the mock. */\nexport interface SentMessage {\n type: string;\n data: Record<string, unknown>;\n}\n\n/** An outbound `protocolRequest` captured by the mock. */\nexport interface ProtocolCall {\n protocol: string;\n method: string;\n params: unknown[];\n}\n\n/** Responder stub for a `protocolRequest(protocol, method, …)`. */\nexport type ProtocolResponder = (params: unknown[]) => unknown;\n\n/** Extra §4 discovery-global fields to publish alongside `transport` on install. */\nexport interface MockHostGlobalExtras {\n runtimeVersion?: string;\n protocolVersion?: string;\n appMountPath?: string;\n}\n\ninterface ImmediatelyRunGlobal extends MockHostGlobalExtras {\n transport?: unknown;\n}\n\n/** The controllable mock host returned by {@link createMockHost}. */\nexport interface MockHost {\n /** The §4 host transport object (also published on the global by `install`). */\n transport: {\n sendMessage(type: string, data?: Record<string, unknown>): void;\n protocolRequest(protocol: string, method: string, params: unknown[]): Promise<unknown>;\n onMessage(handler: (msg: HostMessage) => void): { dispose(): void };\n };\n /** Publish the transport at `globalThis.__immediatelyRun__` (npm-fetched path). */\n install(extras?: MockHostGlobalExtras): void;\n /** Remove the discovery global (call in `afterEach`). */\n uninstall(): void;\n /** Simulate a host → app push; delivered to every live `onMessage` handler. */\n emit(msg: HostMessage): void;\n /** Register a response for a `protocolRequest`; unstubbed calls reject. */\n stubProtocol(protocol: string, method: string, responder: ProtocolResponder): void;\n /** Every `sendMessage` the SDK made, in order. */\n readonly sent: SentMessage[];\n /** Every `protocolRequest` the SDK made, in order. */\n readonly protocolCalls: ProtocolCall[];\n /** Count of live `onMessage` listeners (leak checks). */\n handlerCount(): number;\n}\n\nconst GLOBAL_KEY = '__immediatelyRun__';\n\n/** Build a controllable mock host. Nothing is global until you call `install()`. */\nexport function createMockHost(): MockHost {\n const handlers = new Set<(msg: HostMessage) => void>();\n const sent: SentMessage[] = [];\n const protocolCalls: ProtocolCall[] = [];\n const responders = new Map<string, ProtocolResponder>();\n const key = (protocol: string, method: string) => `${protocol}::${method}`;\n\n const transport: MockHost['transport'] = {\n sendMessage(type, data = {}) {\n sent.push({ type, data });\n },\n protocolRequest(protocol, method, params) {\n protocolCalls.push({ protocol, method, params });\n const responder = responders.get(key(protocol, method));\n if (!responder) {\n return Promise.reject(new Error(`mockHost: no stub for protocolRequest('${protocol}', '${method}')`));\n }\n // Resolve async so callers observe real microtask ordering.\n return Promise.resolve().then(() => responder(params));\n },\n onMessage(handler) {\n handlers.add(handler);\n return { dispose: () => handlers.delete(handler) };\n },\n };\n\n const g = globalThis as { [GLOBAL_KEY]?: ImmediatelyRunGlobal };\n\n return {\n transport,\n install(extras = {}) {\n g[GLOBAL_KEY] = { ...extras, transport };\n },\n uninstall() {\n delete g[GLOBAL_KEY];\n },\n emit(msg) {\n // Copy so a handler that unsubscribes mid-emit doesn't mutate the live set.\n for (const handler of [...handlers]) handler(msg);\n },\n stubProtocol(protocol, method, responder) {\n responders.set(key(protocol, method), responder);\n },\n sent,\n protocolCalls,\n handlerCount: () => handlers.size,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA6EA,MAAM,aAAa;AAGZ,SAAS,iBAA2B;AACzC,QAAM,WAAW,oBAAI,IAAgC;AACrD,QAAM,OAAsB,CAAC;AAC7B,QAAM,gBAAgC,CAAC;AACvC,QAAM,aAAa,oBAAI,IAA+B;AACtD,QAAM,MAAM,CAAC,UAAkB,WAAmB,GAAG,QAAQ,KAAK,MAAM;AAExE,QAAM,YAAmC;AAAA,IACvC,YAAY,MAAM,OAAO,CAAC,GAAG;AAC3B,WAAK,KAAK,EAAE,MAAM,KAAK,CAAC;AAAA,IAC1B;AAAA,IACA,gBAAgB,UAAU,QAAQ,QAAQ;AACxC,oBAAc,KAAK,EAAE,UAAU,QAAQ,OAAO,CAAC;AAC/C,YAAM,YAAY,WAAW,IAAI,IAAI,UAAU,MAAM,CAAC;AACtD,UAAI,CAAC,WAAW;AACd,eAAO,QAAQ,OAAO,IAAI,MAAM,0CAA0C,QAAQ,OAAO,MAAM,IAAI,CAAC;AAAA,MACtG;AAEA,aAAO,QAAQ,QAAQ,EAAE,KAAK,MAAM,UAAU,MAAM,CAAC;AAAA,IACvD;AAAA,IACA,UAAU,SAAS;AACjB,eAAS,IAAI,OAAO;AACpB,aAAO,EAAE,SAAS,MAAM,SAAS,OAAO,OAAO,EAAE;AAAA,IACnD;AAAA,EACF;AAEA,QAAM,IAAI;AAEV,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,SAAS,CAAC,GAAG;AACnB,QAAE,UAAU,IAAI,EAAE,GAAG,QAAQ,UAAU;AAAA,IACzC;AAAA,IACA,YAAY;AACV,aAAO,EAAE,UAAU;AAAA,IACrB;AAAA,IACA,KAAK,KAAK;AAER,iBAAW,WAAW,CAAC,GAAG,QAAQ,EAAG,SAAQ,GAAG;AAAA,IAClD;AAAA,IACA,aAAa,UAAU,QAAQ,WAAW;AACxC,iBAAW,IAAI,IAAI,UAAU,MAAM,GAAG,SAAS;AAAA,IACjD;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,MAAM,SAAS;AAAA,EAC/B;AACF;","names":[]}
package/dist/testing.js CHANGED
@@ -14,9 +14,7 @@ function createMockHost() {
14
14
  protocolCalls.push({ protocol, method, params });
15
15
  const responder = responders.get(key(protocol, method));
16
16
  if (!responder) {
17
- return Promise.reject(
18
- new Error(`mockHost: no stub for protocolRequest('${protocol}', '${method}')`)
19
- );
17
+ return Promise.reject(new Error(`mockHost: no stub for protocolRequest('${protocol}', '${method}')`));
20
18
  }
21
19
  return Promise.resolve().then(() => responder(params));
22
20
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing.ts"],"sourcesContent":["// Public testing utility — `@immediately-run/sdk/testing` (TESTING_AUTOMATION_SPEC\n// §3/§7). A mock host transport so apps built on this SDK can unit-test their host\n// interaction in CI, with no live bundler/host: drive the SDK's REAL transport path\n// by installing this at the §4 discovery global, then `emit()` host→app pushes and\n// assert your app reacts.\n//\n// import { createMockHost } from '@immediately-run/sdk/testing';\n// const host = createMockHost();\n// host.install(); // SDK now resolves this transport\n// host.stubProtocol('spaces', 'list', () => ({ ok: true, data: [] }));\n// // …render your app / call SDK functions…\n// host.emit({ type: 'mount-add', mount: { path: '/spaces/a', type: 'firestore', id: 'a' } });\n// expect(host.sent).toContainEqual({ type: 'request-mounts', data: {} });\n// host.uninstall();\n//\n// It implements the `sandboxUtils` host transport (sendMessage / protocolRequest /\n// onMessage), so tests exercise the actual `transport()` resolution, the\n// `addListener` type-filter, and `protocolRequest` round-trips — higher fidelity\n// than mocking `./sandboxUtils` wholesale.\n\n/** A message pushed host → app, as it arrives on `onMessage`. */\nexport interface HostMessage {\n type: string;\n [k: string]: unknown;\n}\n\n/** An outbound `sendMessage` captured by the mock. */\nexport interface SentMessage {\n type: string;\n data: Record<string, unknown>;\n}\n\n/** An outbound `protocolRequest` captured by the mock. */\nexport interface ProtocolCall {\n protocol: string;\n method: string;\n params: unknown[];\n}\n\n/** Responder stub for a `protocolRequest(protocol, method, …)`. */\nexport type ProtocolResponder = (params: unknown[]) => unknown;\n\n/** Extra §4 discovery-global fields to publish alongside `transport` on install. */\nexport interface MockHostGlobalExtras {\n runtimeVersion?: string;\n protocolVersion?: string;\n appMountPath?: string;\n}\n\ninterface ImmediatelyRunGlobal extends MockHostGlobalExtras {\n transport?: unknown;\n}\n\n/** The controllable mock host returned by {@link createMockHost}. */\nexport interface MockHost {\n /** The §4 host transport object (also published on the global by `install`). */\n transport: {\n sendMessage(type: string, data?: Record<string, unknown>): void;\n protocolRequest(protocol: string, method: string, params: unknown[]): Promise<unknown>;\n onMessage(handler: (msg: HostMessage) => void): { dispose(): void };\n };\n /** Publish the transport at `globalThis.__immediatelyRun__` (npm-fetched path). */\n install(extras?: MockHostGlobalExtras): void;\n /** Remove the discovery global (call in `afterEach`). */\n uninstall(): void;\n /** Simulate a host → app push; delivered to every live `onMessage` handler. */\n emit(msg: HostMessage): void;\n /** Register a response for a `protocolRequest`; unstubbed calls reject. */\n stubProtocol(protocol: string, method: string, responder: ProtocolResponder): void;\n /** Every `sendMessage` the SDK made, in order. */\n readonly sent: SentMessage[];\n /** Every `protocolRequest` the SDK made, in order. */\n readonly protocolCalls: ProtocolCall[];\n /** Count of live `onMessage` listeners (leak checks). */\n handlerCount(): number;\n}\n\nconst GLOBAL_KEY = '__immediatelyRun__';\n\n/** Build a controllable mock host. Nothing is global until you call `install()`. */\nexport function createMockHost(): MockHost {\n const handlers = new Set<(msg: HostMessage) => void>();\n const sent: SentMessage[] = [];\n const protocolCalls: ProtocolCall[] = [];\n const responders = new Map<string, ProtocolResponder>();\n const key = (protocol: string, method: string) => `${protocol}::${method}`;\n\n const transport: MockHost['transport'] = {\n sendMessage(type, data = {}) {\n sent.push({ type, data });\n },\n protocolRequest(protocol, method, params) {\n protocolCalls.push({ protocol, method, params });\n const responder = responders.get(key(protocol, method));\n if (!responder) {\n return Promise.reject(\n new Error(`mockHost: no stub for protocolRequest('${protocol}', '${method}')`),\n );\n }\n // Resolve async so callers observe real microtask ordering.\n return Promise.resolve().then(() => responder(params));\n },\n onMessage(handler) {\n handlers.add(handler);\n return { dispose: () => handlers.delete(handler) };\n },\n };\n\n const g = globalThis as { [GLOBAL_KEY]?: ImmediatelyRunGlobal };\n\n return {\n transport,\n install(extras = {}) {\n g[GLOBAL_KEY] = { ...extras, transport };\n },\n uninstall() {\n delete g[GLOBAL_KEY];\n },\n emit(msg) {\n // Copy so a handler that unsubscribes mid-emit doesn't mutate the live set.\n for (const handler of [...handlers]) handler(msg);\n },\n stubProtocol(protocol, method, responder) {\n responders.set(key(protocol, method), responder);\n },\n sent,\n protocolCalls,\n handlerCount: () => handlers.size,\n };\n}\n"],"mappings":";AA6EA,MAAM,aAAa;AAGZ,SAAS,iBAA2B;AACzC,QAAM,WAAW,oBAAI,IAAgC;AACrD,QAAM,OAAsB,CAAC;AAC7B,QAAM,gBAAgC,CAAC;AACvC,QAAM,aAAa,oBAAI,IAA+B;AACtD,QAAM,MAAM,CAAC,UAAkB,WAAmB,GAAG,QAAQ,KAAK,MAAM;AAExE,QAAM,YAAmC;AAAA,IACvC,YAAY,MAAM,OAAO,CAAC,GAAG;AAC3B,WAAK,KAAK,EAAE,MAAM,KAAK,CAAC;AAAA,IAC1B;AAAA,IACA,gBAAgB,UAAU,QAAQ,QAAQ;AACxC,oBAAc,KAAK,EAAE,UAAU,QAAQ,OAAO,CAAC;AAC/C,YAAM,YAAY,WAAW,IAAI,IAAI,UAAU,MAAM,CAAC;AACtD,UAAI,CAAC,WAAW;AACd,eAAO,QAAQ;AAAA,UACb,IAAI,MAAM,0CAA0C,QAAQ,OAAO,MAAM,IAAI;AAAA,QAC/E;AAAA,MACF;AAEA,aAAO,QAAQ,QAAQ,EAAE,KAAK,MAAM,UAAU,MAAM,CAAC;AAAA,IACvD;AAAA,IACA,UAAU,SAAS;AACjB,eAAS,IAAI,OAAO;AACpB,aAAO,EAAE,SAAS,MAAM,SAAS,OAAO,OAAO,EAAE;AAAA,IACnD;AAAA,EACF;AAEA,QAAM,IAAI;AAEV,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,SAAS,CAAC,GAAG;AACnB,QAAE,UAAU,IAAI,EAAE,GAAG,QAAQ,UAAU;AAAA,IACzC;AAAA,IACA,YAAY;AACV,aAAO,EAAE,UAAU;AAAA,IACrB;AAAA,IACA,KAAK,KAAK;AAER,iBAAW,WAAW,CAAC,GAAG,QAAQ,EAAG,SAAQ,GAAG;AAAA,IAClD;AAAA,IACA,aAAa,UAAU,QAAQ,WAAW;AACxC,iBAAW,IAAI,IAAI,UAAU,MAAM,GAAG,SAAS;AAAA,IACjD;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,MAAM,SAAS;AAAA,EAC/B;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/testing.ts"],"sourcesContent":["// Public testing utility — `@immediately-run/sdk/testing` (TESTING_AUTOMATION_SPEC\n// §3/§7). A mock host transport so apps built on this SDK can unit-test their host\n// interaction in CI, with no live bundler/host: drive the SDK's REAL transport path\n// by installing this at the §4 discovery global, then `emit()` host→app pushes and\n// assert your app reacts.\n//\n// import { createMockHost } from '@immediately-run/sdk/testing';\n// const host = createMockHost();\n// host.install(); // SDK now resolves this transport\n// host.stubProtocol('spaces', 'list', () => ({ ok: true, data: [] }));\n// // …render your app / call SDK functions…\n// host.emit({ type: 'mount-add', mount: { path: '/spaces/a', type: 'firestore', id: 'a' } });\n// expect(host.sent).toContainEqual({ type: 'request-mounts', data: {} });\n// host.uninstall();\n//\n// It implements the `sandboxUtils` host transport (sendMessage / protocolRequest /\n// onMessage), so tests exercise the actual `transport()` resolution, the\n// `addListener` type-filter, and `protocolRequest` round-trips — higher fidelity\n// than mocking `./sandboxUtils` wholesale.\n\n/** A message pushed host → app, as it arrives on `onMessage`. */\nexport interface HostMessage {\n type: string;\n [k: string]: unknown;\n}\n\n/** An outbound `sendMessage` captured by the mock. */\nexport interface SentMessage {\n type: string;\n data: Record<string, unknown>;\n}\n\n/** An outbound `protocolRequest` captured by the mock. */\nexport interface ProtocolCall {\n protocol: string;\n method: string;\n params: unknown[];\n}\n\n/** Responder stub for a `protocolRequest(protocol, method, …)`. */\nexport type ProtocolResponder = (params: unknown[]) => unknown;\n\n/** Extra §4 discovery-global fields to publish alongside `transport` on install. */\nexport interface MockHostGlobalExtras {\n runtimeVersion?: string;\n protocolVersion?: string;\n appMountPath?: string;\n}\n\ninterface ImmediatelyRunGlobal extends MockHostGlobalExtras {\n transport?: unknown;\n}\n\n/** The controllable mock host returned by {@link createMockHost}. */\nexport interface MockHost {\n /** The §4 host transport object (also published on the global by `install`). */\n transport: {\n sendMessage(type: string, data?: Record<string, unknown>): void;\n protocolRequest(protocol: string, method: string, params: unknown[]): Promise<unknown>;\n onMessage(handler: (msg: HostMessage) => void): { dispose(): void };\n };\n /** Publish the transport at `globalThis.__immediatelyRun__` (npm-fetched path). */\n install(extras?: MockHostGlobalExtras): void;\n /** Remove the discovery global (call in `afterEach`). */\n uninstall(): void;\n /** Simulate a host → app push; delivered to every live `onMessage` handler. */\n emit(msg: HostMessage): void;\n /** Register a response for a `protocolRequest`; unstubbed calls reject. */\n stubProtocol(protocol: string, method: string, responder: ProtocolResponder): void;\n /** Every `sendMessage` the SDK made, in order. */\n readonly sent: SentMessage[];\n /** Every `protocolRequest` the SDK made, in order. */\n readonly protocolCalls: ProtocolCall[];\n /** Count of live `onMessage` listeners (leak checks). */\n handlerCount(): number;\n}\n\nconst GLOBAL_KEY = '__immediatelyRun__';\n\n/** Build a controllable mock host. Nothing is global until you call `install()`. */\nexport function createMockHost(): MockHost {\n const handlers = new Set<(msg: HostMessage) => void>();\n const sent: SentMessage[] = [];\n const protocolCalls: ProtocolCall[] = [];\n const responders = new Map<string, ProtocolResponder>();\n const key = (protocol: string, method: string) => `${protocol}::${method}`;\n\n const transport: MockHost['transport'] = {\n sendMessage(type, data = {}) {\n sent.push({ type, data });\n },\n protocolRequest(protocol, method, params) {\n protocolCalls.push({ protocol, method, params });\n const responder = responders.get(key(protocol, method));\n if (!responder) {\n return Promise.reject(new Error(`mockHost: no stub for protocolRequest('${protocol}', '${method}')`));\n }\n // Resolve async so callers observe real microtask ordering.\n return Promise.resolve().then(() => responder(params));\n },\n onMessage(handler) {\n handlers.add(handler);\n return { dispose: () => handlers.delete(handler) };\n },\n };\n\n const g = globalThis as { [GLOBAL_KEY]?: ImmediatelyRunGlobal };\n\n return {\n transport,\n install(extras = {}) {\n g[GLOBAL_KEY] = { ...extras, transport };\n },\n uninstall() {\n delete g[GLOBAL_KEY];\n },\n emit(msg) {\n // Copy so a handler that unsubscribes mid-emit doesn't mutate the live set.\n for (const handler of [...handlers]) handler(msg);\n },\n stubProtocol(protocol, method, responder) {\n responders.set(key(protocol, method), responder);\n },\n sent,\n protocolCalls,\n handlerCount: () => handlers.size,\n };\n}\n"],"mappings":";AA6EA,MAAM,aAAa;AAGZ,SAAS,iBAA2B;AACzC,QAAM,WAAW,oBAAI,IAAgC;AACrD,QAAM,OAAsB,CAAC;AAC7B,QAAM,gBAAgC,CAAC;AACvC,QAAM,aAAa,oBAAI,IAA+B;AACtD,QAAM,MAAM,CAAC,UAAkB,WAAmB,GAAG,QAAQ,KAAK,MAAM;AAExE,QAAM,YAAmC;AAAA,IACvC,YAAY,MAAM,OAAO,CAAC,GAAG;AAC3B,WAAK,KAAK,EAAE,MAAM,KAAK,CAAC;AAAA,IAC1B;AAAA,IACA,gBAAgB,UAAU,QAAQ,QAAQ;AACxC,oBAAc,KAAK,EAAE,UAAU,QAAQ,OAAO,CAAC;AAC/C,YAAM,YAAY,WAAW,IAAI,IAAI,UAAU,MAAM,CAAC;AACtD,UAAI,CAAC,WAAW;AACd,eAAO,QAAQ,OAAO,IAAI,MAAM,0CAA0C,QAAQ,OAAO,MAAM,IAAI,CAAC;AAAA,MACtG;AAEA,aAAO,QAAQ,QAAQ,EAAE,KAAK,MAAM,UAAU,MAAM,CAAC;AAAA,IACvD;AAAA,IACA,UAAU,SAAS;AACjB,eAAS,IAAI,OAAO;AACpB,aAAO,EAAE,SAAS,MAAM,SAAS,OAAO,OAAO,EAAE;AAAA,IACnD;AAAA,EACF;AAEA,QAAM,IAAI;AAEV,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,SAAS,CAAC,GAAG;AACnB,QAAE,UAAU,IAAI,EAAE,GAAG,QAAQ,UAAU;AAAA,IACzC;AAAA,IACA,YAAY;AACV,aAAO,EAAE,UAAU;AAAA,IACrB;AAAA,IACA,KAAK,KAAK;AAER,iBAAW,WAAW,CAAC,GAAG,QAAQ,EAAG,SAAQ,GAAG;AAAA,IAClD;AAAA,IACA,aAAa,UAAU,QAAQ,WAAW;AACxC,iBAAW,IAAI,IAAI,UAAU,MAAM,GAAG,SAAS;AAAA,IACjD;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,MAAM,SAAS;AAAA,EAC/B;AACF;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/theme.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_THEME, REQUEST_THEME, THEME } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/**\n * The host UI theme, mirrored from the immediately.run host window into the\n * sandbox. Your app can read this to render in step with the host chrome\n * (light / dark).\n *\n * This is the baseline `theme:read` capability — every app may read it. Changing\n * the host theme is a separate, elevated action (`theme:set`), available only to\n * the theme-toggle system app.\n */\nexport type HostTheme = 'light' | 'dark';\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `theme` and\n// answers `request-theme` (wire format: site-main channelBridge.ts). The host's\n// default before it reports is `dark` (sandbox themeState.DEFAULT_THEME).\nconst channel = createPushChannel<HostTheme>({\n pushType: THEME,\n requestType: REQUEST_THEME,\n initial: 'dark',\n parse: (msg) => (msg.theme === 'light' || msg.theme === 'dark' ? msg.theme : undefined),\n});\n\n/**\n * Returns the current host theme. Poll this for a one-off read; use\n * {@link onHostThemeChange} or {@link useHostTheme} to react to changes.\n */\nexport const getHostTheme = (): HostTheme => channel.get();\n\n/**\n * Subscribe to host theme changes. The listener is invoked immediately with the\n * current theme, then again on every change. Returns an unsubscribe fn.\n */\nexport const onHostThemeChange = (listener: (theme: HostTheme) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current host theme, re-rendering when it changes.\n * The recommended way to implement an app's own `useTheme`: follow the host,\n * allow a local override.\n */\nexport const useHostTheme = (): HostTheme => channel.use();\n\n/**\n * Set the host UI theme — the ELEVATED `theme:set` action (§8.5). The host\n * applies it and re-pushes the new value to every `theme:read` iframe, so your\n * own {@link useHostTheme} confirms the change (the loop closes with no special\n * case). Only a grant holding `theme:set` (e.g. the theme-toggle system app) may\n * call this; any other app is rejected host-side with a `forbidden`\n * {@link Error} (carrying `.code`), regardless of what the app claims. Update\n * optimistically and let the re-push confirm.\n */\nexport const setHostTheme = async (theme: HostTheme): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_THEME], 'set', [{ theme }])) as\n | { ok: true; data?: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'setHostTheme failed') as Error & {\n code?: string;\n };\n err.code = (res && 'code' in res ? res.code : undefined) ?? 'unknown';\n throw err;\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAkC;AAClC,0BAAgC;AAChC,sBAAqD;AACrD,6BAAwB;AAgBxB,MAAM,cAAU,sCAA6B;AAAA,EAC3C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,IAAI,UAAU,WAAW,IAAI,UAAU,SAAS,IAAI,QAAQ;AAC/E,CAAC;AAMM,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAMlD,MAAM,oBAAoB,CAAC,aAChC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAWlD,MAAM,eAAe,OAAO,UAAoC;AACrE,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,8BAAc,GAAG,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;AAI9E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,qBAAqB;AAG3D,QAAI,QAAQ,OAAO,UAAU,MAAM,IAAI,OAAO,WAAc;AAC5D,UAAM;AAAA,EACR;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/theme.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_THEME, REQUEST_THEME, THEME } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/**\n * The host UI theme, mirrored from the immediately.run host window into the\n * sandbox. Your app can read this to render in step with the host chrome\n * (light / dark).\n *\n * This is the baseline `theme:read` capability — every app may read it. Changing\n * the host theme is a separate, elevated action (`theme:set`), available only to\n * the theme-toggle system app.\n */\nexport type HostTheme = 'light' | 'dark';\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `theme` and\n// answers `request-theme` (wire format: site-main channelBridge.ts). The host's\n// default before it reports is `dark` (sandbox themeState.DEFAULT_THEME).\nconst channel = createPushChannel<HostTheme>({\n pushType: THEME,\n requestType: REQUEST_THEME,\n initial: 'dark',\n parse: (msg) => (msg.theme === 'light' || msg.theme === 'dark' ? msg.theme : undefined),\n});\n\n/**\n * Returns the current host theme. Poll this for a one-off read; use\n * {@link onHostThemeChange} or {@link useHostTheme} to react to changes.\n */\nexport const getHostTheme = (): HostTheme => channel.get();\n\n/**\n * Subscribe to host theme changes. The listener is invoked immediately with the\n * current theme, then again on every change. Returns an unsubscribe fn.\n */\nexport const onHostThemeChange = (listener: (theme: HostTheme) => void): (() => void) => channel.onChange(listener);\n\n/**\n * React hook returning the current host theme, re-rendering when it changes.\n * The recommended way to implement an app's own `useTheme`: follow the host,\n * allow a local override.\n */\nexport const useHostTheme = (): HostTheme => channel.use();\n\n/**\n * Set the host UI theme — the ELEVATED `theme:set` action (§8.5). The host\n * applies it and re-pushes the new value to every `theme:read` iframe, so your\n * own {@link useHostTheme} confirms the change (the loop closes with no special\n * case). Only a grant holding `theme:set` (e.g. the theme-toggle system app) may\n * call this; any other app is rejected host-side with a `forbidden`\n * {@link Error} (carrying `.code`), regardless of what the app claims. Update\n * optimistically and let the re-push confirm.\n */\nexport const setHostTheme = async (theme: HostTheme): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_THEME], 'set', [{ theme }])) as\n | { ok: true; data?: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'setHostTheme failed') as Error & {\n code?: string;\n };\n err.code = (res && 'code' in res ? res.code : undefined) ?? 'unknown';\n throw err;\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAkC;AAClC,0BAAgC;AAChC,sBAAqD;AACrD,6BAAwB;AAgBxB,MAAM,cAAU,sCAA6B;AAAA,EAC3C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,IAAI,UAAU,WAAW,IAAI,UAAU,SAAS,IAAI,QAAQ;AAC/E,CAAC;AAMM,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAMlD,MAAM,oBAAoB,CAAC,aAAuD,QAAQ,SAAS,QAAQ;AAO3G,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAWlD,MAAM,eAAe,OAAO,UAAoC;AACrE,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,8BAAc,GAAG,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;AAI9E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,qBAAqB;AAG3D,QAAI,QAAQ,OAAO,UAAU,MAAM,IAAI,OAAO,WAAc;AAC5D,UAAM;AAAA,EACR;AACF;","names":[]}
package/dist/theme.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/theme.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_THEME, REQUEST_THEME, THEME } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/**\n * The host UI theme, mirrored from the immediately.run host window into the\n * sandbox. Your app can read this to render in step with the host chrome\n * (light / dark).\n *\n * This is the baseline `theme:read` capability — every app may read it. Changing\n * the host theme is a separate, elevated action (`theme:set`), available only to\n * the theme-toggle system app.\n */\nexport type HostTheme = 'light' | 'dark';\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `theme` and\n// answers `request-theme` (wire format: site-main channelBridge.ts). The host's\n// default before it reports is `dark` (sandbox themeState.DEFAULT_THEME).\nconst channel = createPushChannel<HostTheme>({\n pushType: THEME,\n requestType: REQUEST_THEME,\n initial: 'dark',\n parse: (msg) => (msg.theme === 'light' || msg.theme === 'dark' ? msg.theme : undefined),\n});\n\n/**\n * Returns the current host theme. Poll this for a one-off read; use\n * {@link onHostThemeChange} or {@link useHostTheme} to react to changes.\n */\nexport const getHostTheme = (): HostTheme => channel.get();\n\n/**\n * Subscribe to host theme changes. The listener is invoked immediately with the\n * current theme, then again on every change. Returns an unsubscribe fn.\n */\nexport const onHostThemeChange = (listener: (theme: HostTheme) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current host theme, re-rendering when it changes.\n * The recommended way to implement an app's own `useTheme`: follow the host,\n * allow a local override.\n */\nexport const useHostTheme = (): HostTheme => channel.use();\n\n/**\n * Set the host UI theme — the ELEVATED `theme:set` action (§8.5). The host\n * applies it and re-pushes the new value to every `theme:read` iframe, so your\n * own {@link useHostTheme} confirms the change (the loop closes with no special\n * case). Only a grant holding `theme:set` (e.g. the theme-toggle system app) may\n * call this; any other app is rejected host-side with a `forbidden`\n * {@link Error} (carrying `.code`), regardless of what the app claims. Update\n * optimistically and let the re-push confirm.\n */\nexport const setHostTheme = async (theme: HostTheme): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_THEME], 'set', [{ theme }])) as\n | { ok: true; data?: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'setHostTheme failed') as Error & {\n code?: string;\n };\n err.code = (res && 'code' in res ? res.code : undefined) ?? 'unknown';\n throw err;\n }\n};\n"],"mappings":";AAAA,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAChC,SAAS,gBAAgB,eAAe,aAAa;AACrD,SAAS,eAAe;AAgBxB,MAAM,UAAU,kBAA6B;AAAA,EAC3C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,IAAI,UAAU,WAAW,IAAI,UAAU,SAAS,IAAI,QAAQ;AAC/E,CAAC;AAMM,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAMlD,MAAM,oBAAoB,CAAC,aAChC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAWlD,MAAM,eAAe,OAAO,UAAoC;AACrE,QAAM,MAAO,MAAM,gBAAgB,QAAQ,cAAc,GAAG,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;AAI9E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,qBAAqB;AAG3D,QAAI,QAAQ,OAAO,UAAU,MAAM,IAAI,OAAO,WAAc;AAC5D,UAAM;AAAA,EACR;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/theme.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_THEME, REQUEST_THEME, THEME } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/**\n * The host UI theme, mirrored from the immediately.run host window into the\n * sandbox. Your app can read this to render in step with the host chrome\n * (light / dark).\n *\n * This is the baseline `theme:read` capability — every app may read it. Changing\n * the host theme is a separate, elevated action (`theme:set`), available only to\n * the theme-toggle system app.\n */\nexport type HostTheme = 'light' | 'dark';\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `theme` and\n// answers `request-theme` (wire format: site-main channelBridge.ts). The host's\n// default before it reports is `dark` (sandbox themeState.DEFAULT_THEME).\nconst channel = createPushChannel<HostTheme>({\n pushType: THEME,\n requestType: REQUEST_THEME,\n initial: 'dark',\n parse: (msg) => (msg.theme === 'light' || msg.theme === 'dark' ? msg.theme : undefined),\n});\n\n/**\n * Returns the current host theme. Poll this for a one-off read; use\n * {@link onHostThemeChange} or {@link useHostTheme} to react to changes.\n */\nexport const getHostTheme = (): HostTheme => channel.get();\n\n/**\n * Subscribe to host theme changes. The listener is invoked immediately with the\n * current theme, then again on every change. Returns an unsubscribe fn.\n */\nexport const onHostThemeChange = (listener: (theme: HostTheme) => void): (() => void) => channel.onChange(listener);\n\n/**\n * React hook returning the current host theme, re-rendering when it changes.\n * The recommended way to implement an app's own `useTheme`: follow the host,\n * allow a local override.\n */\nexport const useHostTheme = (): HostTheme => channel.use();\n\n/**\n * Set the host UI theme — the ELEVATED `theme:set` action (§8.5). The host\n * applies it and re-pushes the new value to every `theme:read` iframe, so your\n * own {@link useHostTheme} confirms the change (the loop closes with no special\n * case). Only a grant holding `theme:set` (e.g. the theme-toggle system app) may\n * call this; any other app is rejected host-side with a `forbidden`\n * {@link Error} (carrying `.code`), regardless of what the app claims. Update\n * optimistically and let the re-push confirm.\n */\nexport const setHostTheme = async (theme: HostTheme): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_THEME], 'set', [{ theme }])) as\n | { ok: true; data?: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'setHostTheme failed') as Error & {\n code?: string;\n };\n err.code = (res && 'code' in res ? res.code : undefined) ?? 'unknown';\n throw err;\n }\n};\n"],"mappings":";AAAA,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAChC,SAAS,gBAAgB,eAAe,aAAa;AACrD,SAAS,eAAe;AAgBxB,MAAM,UAAU,kBAA6B;AAAA,EAC3C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,IAAI,UAAU,WAAW,IAAI,UAAU,SAAS,IAAI,QAAQ;AAC/E,CAAC;AAMM,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAMlD,MAAM,oBAAoB,CAAC,aAAuD,QAAQ,SAAS,QAAQ;AAO3G,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAWlD,MAAM,eAAe,OAAO,UAAoC;AACrE,QAAM,MAAO,MAAM,gBAAgB,QAAQ,cAAc,GAAG,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;AAI9E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,qBAAqB;AAG3D,QAAI,QAAQ,OAAO,UAAU,MAAM,IAAI,OAAO,WAAc;AAC5D,UAAM;AAAA,EACR;AACF;","names":[]}
package/dist/urlUtils.cjs CHANGED
@@ -42,9 +42,7 @@ const getOuterHostname = (outerHref) => {
42
42
  const url = new URL(outerHref);
43
43
  return `${url.protocol}//${url.host}`;
44
44
  };
45
- const getSearchParams = (search) => Object.fromEntries(
46
- [...new URLSearchParams(search ?? window.location.search).entries()]
47
- );
45
+ const getSearchParams = (search) => Object.fromEntries([...new URLSearchParams(search ?? window.location.search).entries()]);
48
46
  const splitHash = (target) => {
49
47
  const i = target.indexOf("#");
50
48
  return i === -1 ? [target, ""] : [target.slice(0, i), target.slice(i + 1)];
@@ -79,22 +77,13 @@ const repositoryPrefixURL = (outerHref, navigationState) => constructUrl(outerHr
79
77
  const constructOuterUrl = (previousOuterHref, sandboxTarget, navigationState, addFilesPrefix = true) => {
80
78
  if (isAbsolutePath(sandboxTarget)) {
81
79
  const [pathPart, hash] = splitHash(sandboxTarget);
82
- return constructUrl(
83
- previousOuterHref,
84
- {
85
- ...navigationState,
86
- sandboxPath: addFilesPrefix ? (0, import_pathUtils.joinPaths)(FILES_PREFIX, pathPart) : pathPart,
87
- hash
88
- }
89
- );
80
+ return constructUrl(previousOuterHref, {
81
+ ...navigationState,
82
+ sandboxPath: addFilesPrefix ? (0, import_pathUtils.joinPaths)(FILES_PREFIX, pathPart) : pathPart,
83
+ hash
84
+ });
90
85
  }
91
- return new URL(
92
- sandboxTarget,
93
- constructUrl(
94
- previousOuterHref,
95
- navigationState
96
- )
97
- ).toString();
86
+ return new URL(sandboxTarget, constructUrl(previousOuterHref, navigationState)).toString();
98
87
  };
99
88
  const isInternalHref = (outerHref, target, navigationState) => {
100
89
  const parsedUrl = maybeParseUrl(target);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { APP_ROOT, underAppRoot } from \"@immediately-run/platform-constants\";\n\nimport { joinPaths } from \"./pathUtils\";\nimport { NavigationState, PathState } from \"./TinkerableContext\";\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem, and the join\n * into it — both now defined ONCE in `@immediately-run/platform-constants` (R3-275)\n * and re-exported here so every import site is unchanged.\n *\n * They were declared here AND in `sandbox/src/fsLayout.ts`, with two different\n * implementations of the join (this one over `joinPaths`, the sandbox's hand-rolled).\n * The two agreed; nothing made them agree — and the metadata key space is derived\n * from this constant on both sides, so a drift would have split that key space\n * silently. The package's tests replicate BOTH implementations, quirks included.\n *\n * The sandbox fs is rooted at `/` (so apps can reach dynamic mounts like\n * `/firestore`), with the repo mounted at `APP_ROOT`. URL subpaths are\n * repo-relative, so the file router resolves them under it.\n */\nexport { APP_ROOT, underAppRoot };\n\n/**\n * The origin every outer URL is rebuilt on — `protocol//host`, **port included**.\n *\n * `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default\n * 80/443, so a production URL is byte-identical either way). Using `hostname` here was\n * invisible on `https://immediately.run` and broke every routed link under local dev on any\n * port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.\n *\n * Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is\n * built from this, and `isInternalHref` decides by prefix-matching against it — so an\n * absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain\n * `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of\n * routing: the app appeared to \"reload\" on an ordinary internal link.\n */\nexport const getOuterHostname = (outerHref:string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.host}`;\n}\n\nexport const getSearchParams = (search?: string): Record<string, string> => Object.fromEntries(\n [...(new URLSearchParams(search ?? window.location.search).entries())]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split(\"#\")\n if (prehash) {\n let [path, search] = prehash.split(\"?\")\n if (path) {\n newNavigation.sandboxPath = path\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation\n}\n\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n}\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\n/**\n * The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to\n * decide whether an absolute href is \"this app\" or somewhere else.\n *\n * `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,\n * and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,\n * the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute\n * in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away\n * instead of routing. A page with a fragment quietly broke its own links.\n */\nexport const repositoryPrefixURL = (outerHref:string, navigationState: NavigationState) => constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: '',\n hash: '',\n search: '',\n });\n\nexport const constructOuterUrl = (previousOuterHref:string, sandboxTarget:string, navigationState: NavigationState, addFilesPrefix=true):string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(\n previousOuterHref,\n {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n })\n }\n return (\n new URL(\n sandboxTarget,\n constructUrl(\n previousOuterHref,\n navigationState\n )\n )\n ).toString();\n}\n\nexport const isInternalHref = (outerHref:string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n}\n\nexport type PathSegment = {\n name: string,\n pattern: string,\n transform?: (pathSegment: string) => string,\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean\n // Inverse of `transform`, applied when BUILDING a url (`constructUrl`). Without it a\n // decoded ref would be written back raw and split into two segments again.\n encode?: (value: string) => string\n}\n\n// One URL path segment is \"anything but a slash\". The previous patterns instead\n// ENUMERATED the allowed characters (`[a-zA-Z0-9-_]+`), which silently rejected three\n// real shapes — and rejection here is not an error, it is a whole-match failure that\n// makes `parsePath` return every field EMPTY, so the app's navigation state just goes\n// blank with nothing logged:\n//\n// /…/repo/v1.2.3/… a semver TAG (dot)\n// /…/foo.js/main/… a dotted REPOSITORY name (dot — very common)\n// /…/repo/claude%2Fx/… a `/`-containing REF, percent-encoded as one segment\n//\n// The third is the one that motivated this (a ref may contain `/`; the host now encodes\n// it — see site-main `encodeRef`), but the first two were already broken and are the\n// same mistake. `[^/]+` is both the fix and the accurate description of a segment.\n/** Percent-decode one path segment, tolerating a malformed escape: this is untrusted\n * URL input, and `decodeURIComponent('%zz')` throws. A bad ref must not blank out the\n * whole navigation state. */\nconst decodeSegment = (value: string): string => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n};\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[^/]+' },\n { name: 'namespace', pattern: '[^/]+' },\n { name: 'repository', pattern: '[^/]+' },\n // The ref is percent-encoded by the host so a `/` inside it stays ONE segment; decode\n // on the way in and re-encode on the way out (`encode`), so app code always sees the\n // real ref (`claude/x`) and never the wire form.\n { name: 'ref', pattern: '[^/]+', transform: decodeSegment, encode: encodeURIComponent },\n { name: 'sandboxPath', pattern: '.*', transform: s => `/${s}`, optionalLeadingSlash: true }\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash\n ? `(?:\\/(?<${name}>${pattern}))?`\n : `\\/(?<${name}>${pattern})`\n ).join('') +\n \"$\"\n);\n\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n}\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState\n}\n\nconst stripSlashPrefix = (s: string): string => s.startsWith('/') ? s.substring(1) : s;\n\nexport const constructUrl = (outerHref:string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name, encode }) => {\n const value = stripSlashPrefix(navigationState[name] ?? '');\n // Encode AFTER stripping the delimiter slash: `encode` is per-segment, and the\n // sandboxPath (which has no `encode`) keeps its own internal slashes.\n return encode ? encode(value) : value;\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`\n if (navigationState.search) {\n url += '?' + navigationState.search\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash\n }\n return url;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { APP_ROOT, underAppRoot } from '@immediately-run/platform-constants';\n\nimport { joinPaths } from './pathUtils';\nimport { NavigationState, PathState } from './TinkerableContext';\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem, and the join\n * into it — both now defined ONCE in `@immediately-run/platform-constants` (R3-275)\n * and re-exported here so every import site is unchanged.\n *\n * They were declared here AND in `sandbox/src/fsLayout.ts`, with two different\n * implementations of the join (this one over `joinPaths`, the sandbox's hand-rolled).\n * The two agreed; nothing made them agree — and the metadata key space is derived\n * from this constant on both sides, so a drift would have split that key space\n * silently. The package's tests replicate BOTH implementations, quirks included.\n *\n * The sandbox fs is rooted at `/` (so apps can reach dynamic mounts like\n * `/firestore`), with the repo mounted at `APP_ROOT`. URL subpaths are\n * repo-relative, so the file router resolves them under it.\n */\nexport { APP_ROOT, underAppRoot };\n\n/**\n * The origin every outer URL is rebuilt on — `protocol//host`, **port included**.\n *\n * `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default\n * 80/443, so a production URL is byte-identical either way). Using `hostname` here was\n * invisible on `https://immediately.run` and broke every routed link under local dev on any\n * port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.\n *\n * Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is\n * built from this, and `isInternalHref` decides by prefix-matching against it — so an\n * absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain\n * `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of\n * routing: the app appeared to \"reload\" on an ordinary internal link.\n */\nexport const getOuterHostname = (outerHref: string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.host}`;\n};\n\nexport const getSearchParams = (search?: string): Record<string, string> =>\n Object.fromEntries([...new URLSearchParams(search ?? window.location.search).entries()]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split('#');\n if (prehash) {\n let [path, search] = prehash.split('?');\n if (path) {\n newNavigation.sandboxPath = path;\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation;\n};\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n};\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\n/**\n * The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to\n * decide whether an absolute href is \"this app\" or somewhere else.\n *\n * `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,\n * and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,\n * the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute\n * in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away\n * instead of routing. A page with a fragment quietly broke its own links.\n */\nexport const repositoryPrefixURL = (outerHref: string, navigationState: NavigationState) =>\n constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: '',\n hash: '',\n search: '',\n });\n\nexport const constructOuterUrl = (\n previousOuterHref: string,\n sandboxTarget: string,\n navigationState: NavigationState,\n addFilesPrefix = true,\n): string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(previousOuterHref, {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n });\n }\n return new URL(sandboxTarget, constructUrl(previousOuterHref, navigationState)).toString();\n};\n\nexport const isInternalHref = (outerHref: string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n};\n\nexport type PathSegment = {\n name: string;\n pattern: string;\n transform?: (pathSegment: string) => string;\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean;\n // Inverse of `transform`, applied when BUILDING a url (`constructUrl`). Without it a\n // decoded ref would be written back raw and split into two segments again.\n encode?: (value: string) => string;\n};\n\n// One URL path segment is \"anything but a slash\". The previous patterns instead\n// ENUMERATED the allowed characters (`[a-zA-Z0-9-_]+`), which silently rejected three\n// real shapes — and rejection here is not an error, it is a whole-match failure that\n// makes `parsePath` return every field EMPTY, so the app's navigation state just goes\n// blank with nothing logged:\n//\n// /…/repo/v1.2.3/… a semver TAG (dot)\n// /…/foo.js/main/… a dotted REPOSITORY name (dot — very common)\n// /…/repo/claude%2Fx/… a `/`-containing REF, percent-encoded as one segment\n//\n// The third is the one that motivated this (a ref may contain `/`; the host now encodes\n// it — see site-main `encodeRef`), but the first two were already broken and are the\n// same mistake. `[^/]+` is both the fix and the accurate description of a segment.\n/** Percent-decode one path segment, tolerating a malformed escape: this is untrusted\n * URL input, and `decodeURIComponent('%zz')` throws. A bad ref must not blank out the\n * whole navigation state. */\nconst decodeSegment = (value: string): string => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n};\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[^/]+' },\n { name: 'namespace', pattern: '[^/]+' },\n { name: 'repository', pattern: '[^/]+' },\n // The ref is percent-encoded by the host so a `/` inside it stays ONE segment; decode\n // on the way in and re-encode on the way out (`encode`), so app code always sees the\n // real ref (`claude/x`) and never the wire form.\n { name: 'ref', pattern: '[^/]+', transform: decodeSegment, encode: encodeURIComponent },\n { name: 'sandboxPath', pattern: '.*', transform: (s) => `/${s}`, optionalLeadingSlash: true },\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash ? `(?:\\/(?<${name}>${pattern}))?` : `\\/(?<${name}>${pattern})`,\n ).join('') +\n '$',\n);\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n};\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState;\n};\n\nconst stripSlashPrefix = (s: string): string => (s.startsWith('/') ? s.substring(1) : s);\n\nexport const constructUrl = (outerHref: string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name, encode }) => {\n const value = stripSlashPrefix(navigationState[name] ?? '');\n // Encode AFTER stripping the delimiter slash: `encode` is per-segment, and the\n // sandboxPath (which has no `encode`) keeps its own internal slashes.\n return encode ? encode(value) : value;\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`;\n if (navigationState.search) {\n url += '?' + navigationState.search;\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash;\n }\n return url;\n};\n"],"mappings":"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","names":[]}
package/dist/urlUtils.js CHANGED
@@ -6,9 +6,7 @@ const getOuterHostname = (outerHref) => {
6
6
  const url = new URL(outerHref);
7
7
  return `${url.protocol}//${url.host}`;
8
8
  };
9
- const getSearchParams = (search) => Object.fromEntries(
10
- [...new URLSearchParams(search ?? window.location.search).entries()]
11
- );
9
+ const getSearchParams = (search) => Object.fromEntries([...new URLSearchParams(search ?? window.location.search).entries()]);
12
10
  const splitHash = (target) => {
13
11
  const i = target.indexOf("#");
14
12
  return i === -1 ? [target, ""] : [target.slice(0, i), target.slice(i + 1)];
@@ -43,22 +41,13 @@ const repositoryPrefixURL = (outerHref, navigationState) => constructUrl(outerHr
43
41
  const constructOuterUrl = (previousOuterHref, sandboxTarget, navigationState, addFilesPrefix = true) => {
44
42
  if (isAbsolutePath(sandboxTarget)) {
45
43
  const [pathPart, hash] = splitHash(sandboxTarget);
46
- return constructUrl(
47
- previousOuterHref,
48
- {
49
- ...navigationState,
50
- sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,
51
- hash
52
- }
53
- );
44
+ return constructUrl(previousOuterHref, {
45
+ ...navigationState,
46
+ sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,
47
+ hash
48
+ });
54
49
  }
55
- return new URL(
56
- sandboxTarget,
57
- constructUrl(
58
- previousOuterHref,
59
- navigationState
60
- )
61
- ).toString();
50
+ return new URL(sandboxTarget, constructUrl(previousOuterHref, navigationState)).toString();
62
51
  };
63
52
  const isInternalHref = (outerHref, target, navigationState) => {
64
53
  const parsedUrl = maybeParseUrl(target);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { APP_ROOT, underAppRoot } from \"@immediately-run/platform-constants\";\n\nimport { joinPaths } from \"./pathUtils\";\nimport { NavigationState, PathState } from \"./TinkerableContext\";\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem, and the join\n * into it — both now defined ONCE in `@immediately-run/platform-constants` (R3-275)\n * and re-exported here so every import site is unchanged.\n *\n * They were declared here AND in `sandbox/src/fsLayout.ts`, with two different\n * implementations of the join (this one over `joinPaths`, the sandbox's hand-rolled).\n * The two agreed; nothing made them agree — and the metadata key space is derived\n * from this constant on both sides, so a drift would have split that key space\n * silently. The package's tests replicate BOTH implementations, quirks included.\n *\n * The sandbox fs is rooted at `/` (so apps can reach dynamic mounts like\n * `/firestore`), with the repo mounted at `APP_ROOT`. URL subpaths are\n * repo-relative, so the file router resolves them under it.\n */\nexport { APP_ROOT, underAppRoot };\n\n/**\n * The origin every outer URL is rebuilt on — `protocol//host`, **port included**.\n *\n * `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default\n * 80/443, so a production URL is byte-identical either way). Using `hostname` here was\n * invisible on `https://immediately.run` and broke every routed link under local dev on any\n * port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.\n *\n * Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is\n * built from this, and `isInternalHref` decides by prefix-matching against it — so an\n * absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain\n * `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of\n * routing: the app appeared to \"reload\" on an ordinary internal link.\n */\nexport const getOuterHostname = (outerHref:string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.host}`;\n}\n\nexport const getSearchParams = (search?: string): Record<string, string> => Object.fromEntries(\n [...(new URLSearchParams(search ?? window.location.search).entries())]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split(\"#\")\n if (prehash) {\n let [path, search] = prehash.split(\"?\")\n if (path) {\n newNavigation.sandboxPath = path\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation\n}\n\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n}\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\n/**\n * The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to\n * decide whether an absolute href is \"this app\" or somewhere else.\n *\n * `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,\n * and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,\n * the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute\n * in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away\n * instead of routing. A page with a fragment quietly broke its own links.\n */\nexport const repositoryPrefixURL = (outerHref:string, navigationState: NavigationState) => constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: '',\n hash: '',\n search: '',\n });\n\nexport const constructOuterUrl = (previousOuterHref:string, sandboxTarget:string, navigationState: NavigationState, addFilesPrefix=true):string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(\n previousOuterHref,\n {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n })\n }\n return (\n new URL(\n sandboxTarget,\n constructUrl(\n previousOuterHref,\n navigationState\n )\n )\n ).toString();\n}\n\nexport const isInternalHref = (outerHref:string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n}\n\nexport type PathSegment = {\n name: string,\n pattern: string,\n transform?: (pathSegment: string) => string,\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean\n // Inverse of `transform`, applied when BUILDING a url (`constructUrl`). Without it a\n // decoded ref would be written back raw and split into two segments again.\n encode?: (value: string) => string\n}\n\n// One URL path segment is \"anything but a slash\". The previous patterns instead\n// ENUMERATED the allowed characters (`[a-zA-Z0-9-_]+`), which silently rejected three\n// real shapes — and rejection here is not an error, it is a whole-match failure that\n// makes `parsePath` return every field EMPTY, so the app's navigation state just goes\n// blank with nothing logged:\n//\n// /…/repo/v1.2.3/… a semver TAG (dot)\n// /…/foo.js/main/… a dotted REPOSITORY name (dot — very common)\n// /…/repo/claude%2Fx/… a `/`-containing REF, percent-encoded as one segment\n//\n// The third is the one that motivated this (a ref may contain `/`; the host now encodes\n// it — see site-main `encodeRef`), but the first two were already broken and are the\n// same mistake. `[^/]+` is both the fix and the accurate description of a segment.\n/** Percent-decode one path segment, tolerating a malformed escape: this is untrusted\n * URL input, and `decodeURIComponent('%zz')` throws. A bad ref must not blank out the\n * whole navigation state. */\nconst decodeSegment = (value: string): string => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n};\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[^/]+' },\n { name: 'namespace', pattern: '[^/]+' },\n { name: 'repository', pattern: '[^/]+' },\n // The ref is percent-encoded by the host so a `/` inside it stays ONE segment; decode\n // on the way in and re-encode on the way out (`encode`), so app code always sees the\n // real ref (`claude/x`) and never the wire form.\n { name: 'ref', pattern: '[^/]+', transform: decodeSegment, encode: encodeURIComponent },\n { name: 'sandboxPath', pattern: '.*', transform: s => `/${s}`, optionalLeadingSlash: true }\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash\n ? `(?:\\/(?<${name}>${pattern}))?`\n : `\\/(?<${name}>${pattern})`\n ).join('') +\n \"$\"\n);\n\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n}\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState\n}\n\nconst stripSlashPrefix = (s: string): string => s.startsWith('/') ? s.substring(1) : s;\n\nexport const constructUrl = (outerHref:string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name, encode }) => {\n const value = stripSlashPrefix(navigationState[name] ?? '');\n // Encode AFTER stripping the delimiter slash: `encode` is per-segment, and the\n // sandboxPath (which has no `encode`) keeps its own internal slashes.\n return encode ? encode(value) : value;\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`\n if (navigationState.search) {\n url += '?' + navigationState.search\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash\n }\n return url;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { APP_ROOT, underAppRoot } from '@immediately-run/platform-constants';\n\nimport { joinPaths } from './pathUtils';\nimport { NavigationState, PathState } from './TinkerableContext';\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem, and the join\n * into it — both now defined ONCE in `@immediately-run/platform-constants` (R3-275)\n * and re-exported here so every import site is unchanged.\n *\n * They were declared here AND in `sandbox/src/fsLayout.ts`, with two different\n * implementations of the join (this one over `joinPaths`, the sandbox's hand-rolled).\n * The two agreed; nothing made them agree — and the metadata key space is derived\n * from this constant on both sides, so a drift would have split that key space\n * silently. The package's tests replicate BOTH implementations, quirks included.\n *\n * The sandbox fs is rooted at `/` (so apps can reach dynamic mounts like\n * `/firestore`), with the repo mounted at `APP_ROOT`. URL subpaths are\n * repo-relative, so the file router resolves them under it.\n */\nexport { APP_ROOT, underAppRoot };\n\n/**\n * The origin every outer URL is rebuilt on — `protocol//host`, **port included**.\n *\n * `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default\n * 80/443, so a production URL is byte-identical either way). Using `hostname` here was\n * invisible on `https://immediately.run` and broke every routed link under local dev on any\n * port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.\n *\n * Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is\n * built from this, and `isInternalHref` decides by prefix-matching against it — so an\n * absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain\n * `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of\n * routing: the app appeared to \"reload\" on an ordinary internal link.\n */\nexport const getOuterHostname = (outerHref: string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.host}`;\n};\n\nexport const getSearchParams = (search?: string): Record<string, string> =>\n Object.fromEntries([...new URLSearchParams(search ?? window.location.search).entries()]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split('#');\n if (prehash) {\n let [path, search] = prehash.split('?');\n if (path) {\n newNavigation.sandboxPath = path;\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation;\n};\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n};\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\n/**\n * The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to\n * decide whether an absolute href is \"this app\" or somewhere else.\n *\n * `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,\n * and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,\n * the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute\n * in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away\n * instead of routing. A page with a fragment quietly broke its own links.\n */\nexport const repositoryPrefixURL = (outerHref: string, navigationState: NavigationState) =>\n constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: '',\n hash: '',\n search: '',\n });\n\nexport const constructOuterUrl = (\n previousOuterHref: string,\n sandboxTarget: string,\n navigationState: NavigationState,\n addFilesPrefix = true,\n): string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(previousOuterHref, {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n });\n }\n return new URL(sandboxTarget, constructUrl(previousOuterHref, navigationState)).toString();\n};\n\nexport const isInternalHref = (outerHref: string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n};\n\nexport type PathSegment = {\n name: string;\n pattern: string;\n transform?: (pathSegment: string) => string;\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean;\n // Inverse of `transform`, applied when BUILDING a url (`constructUrl`). Without it a\n // decoded ref would be written back raw and split into two segments again.\n encode?: (value: string) => string;\n};\n\n// One URL path segment is \"anything but a slash\". The previous patterns instead\n// ENUMERATED the allowed characters (`[a-zA-Z0-9-_]+`), which silently rejected three\n// real shapes — and rejection here is not an error, it is a whole-match failure that\n// makes `parsePath` return every field EMPTY, so the app's navigation state just goes\n// blank with nothing logged:\n//\n// /…/repo/v1.2.3/… a semver TAG (dot)\n// /…/foo.js/main/… a dotted REPOSITORY name (dot — very common)\n// /…/repo/claude%2Fx/… a `/`-containing REF, percent-encoded as one segment\n//\n// The third is the one that motivated this (a ref may contain `/`; the host now encodes\n// it — see site-main `encodeRef`), but the first two were already broken and are the\n// same mistake. `[^/]+` is both the fix and the accurate description of a segment.\n/** Percent-decode one path segment, tolerating a malformed escape: this is untrusted\n * URL input, and `decodeURIComponent('%zz')` throws. A bad ref must not blank out the\n * whole navigation state. */\nconst decodeSegment = (value: string): string => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n};\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[^/]+' },\n { name: 'namespace', pattern: '[^/]+' },\n { name: 'repository', pattern: '[^/]+' },\n // The ref is percent-encoded by the host so a `/` inside it stays ONE segment; decode\n // on the way in and re-encode on the way out (`encode`), so app code always sees the\n // real ref (`claude/x`) and never the wire form.\n { name: 'ref', pattern: '[^/]+', transform: decodeSegment, encode: encodeURIComponent },\n { name: 'sandboxPath', pattern: '.*', transform: (s) => `/${s}`, optionalLeadingSlash: true },\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash ? `(?:\\/(?<${name}>${pattern}))?` : `\\/(?<${name}>${pattern})`,\n ).join('') +\n '$',\n);\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n};\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState;\n};\n\nconst stripSlashPrefix = (s: string): string => (s.startsWith('/') ? s.substring(1) : s);\n\nexport const constructUrl = (outerHref: string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name, encode }) => {\n const value = stripSlashPrefix(navigationState[name] ?? '');\n // Encode AFTER stripping the delimiter slash: `encode` is per-segment, and the\n // sandboxPath (which has no `encode`) keeps its own internal slashes.\n return encode ? encode(value) : value;\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`;\n if (navigationState.search) {\n url += '?' + navigationState.search;\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash;\n }\n return url;\n};\n"],"mappings":"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","names":[]}
package/dist/vcs.cjs CHANGED
@@ -31,9 +31,7 @@ var import_sandboxUtils = require("./sandboxUtils");
31
31
  var import_protocol = require("./generated/protocol");
32
32
  var import_protocolSchemes = require("./protocolSchemes");
33
33
  const EMPTY = { changes: [], branch: null, prs: [], diffLoading: false };
34
- const isChangeArray = (v) => Array.isArray(v) && v.every(
35
- (c) => !!c && typeof c.path === "string" && typeof c.status === "string"
36
- );
34
+ const isChangeArray = (v) => Array.isArray(v) && v.every((c) => !!c && typeof c.path === "string" && typeof c.status === "string");
37
35
  const channel = (0, import_pushChannel.createPushChannel)({
38
36
  pushType: import_protocol.VCS_STATE,
39
37
  requestType: import_protocol.REQUEST_VCS_STATE,
package/dist/vcs.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/vcs.ts"],"sourcesContent":["// The `vcs:read` / `vcs:reset` source-control surface — app-facing wrappers\n// (UI_AS_APPS_SPEC §5.3 Recipe A read channel + §8.4 Recipe B actions;\n// migrate-sidebars-to-apps Phase 05; roadmap R3-52).\n//\n// A first-party contribute panel (the `panel.contribute` app, Phase 06) reads the\n// live source-control state the native `SourceControlPanel` shows — the working-tree\n// diff summary, the branch lineage, and the open-PR list — and can trigger a\n// refresh/reset. The host derives all of it from authenticated GitHub calls +\n// the COW layers; NONE of the underlying `DiffResult` / `FileSystem` / OAuth token\n// ever crosses the boundary (§8.10) — the wire carries only the plain-JSON\n// {@link VcsState} the host projects.\n//\n// Read side: a Recipe-A push channel, the same get/onChange/use trio as\n// `editorContext` / `diagnostics`. Inert until the host wires the channel\n// (site-main `channelBridge`); an app without `vcs:read` simply sees the empty\n// initial. Action side: `protocol-vcs` requests gated host-side — `refreshDiff` /\n// `refreshPRs` by `vcs:read`, `resetWorkingTree` by first-party-only `vcs:reset`.\nimport { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_VCS, REQUEST_VCS_STATE, VCS_STATE } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** One changed path in the working tree (vs. the loaded ref). `status` mirrors the\n * host `DiffResult` change kinds; `path` is repo-relative. Contents are NOT\n * carried — the native panel showed path + status, and a per-file content diff is\n * a deferred follow-up (plan step 6). */\nexport interface VcsChange {\n path: string;\n status: 'created' | 'modified' | 'deleted';\n}\n\n/** The branch the working tree sits on and the upstream it diverged from\n * (host `BranchInfo`, §15.1). `null` until the user is on a immediately.run-created\n * branch. `upstreamPushable` is `null` while push access is still being probed. */\nexport interface VcsBranch {\n name: string;\n parentRepo: string;\n parentRef: string;\n parentCommitSha: string;\n upstreamPushable: boolean | null;\n}\n\n/** One pull request open from the current branch (host `BranchPR`). */\nexport interface VcsPR {\n number: number;\n url: string;\n title: string;\n state: 'open' | 'closed' | 'merged';\n draft: boolean;\n}\n\n/** The whole source-control snapshot the host projects to a `vcs:read` frame.\n * Plain JSON — never a `DiffResult` / `FileSystem` / `Journal`. */\nexport interface VcsState {\n changes: VcsChange[];\n branch: VcsBranch | null;\n prs: VcsPR[];\n /** True while the host is recomputing the diff — the panel shows a spinner\n * without a separate request (plan gotcha). */\n diffLoading: boolean;\n}\n\n/** Value before the host answers — also the value when the app may not read the\n * channel (no `vcs:read`): an empty, branch-less snapshot. */\nconst EMPTY: VcsState = { changes: [], branch: null, prs: [], diffLoading: false };\n\nconst isChangeArray = (v: unknown): v is VcsChange[] =>\n Array.isArray(v) &&\n v.every(\n (c) =>\n !!c &&\n typeof (c as VcsChange).path === 'string' &&\n typeof (c as VcsChange).status === 'string',\n );\n\nconst channel = createPushChannel<VcsState>({\n pushType: VCS_STATE,\n requestType: REQUEST_VCS_STATE,\n initial: EMPTY,\n parse: (msg) => {\n // Require a well-formed `changes` array; tolerate an absent branch/prs. A\n // malformed push is ignored (returns undefined) so the last good state stands.\n if (!isChangeArray(msg.changes)) return undefined;\n const branch =\n msg.branch && typeof msg.branch === 'object' ? (msg.branch as VcsBranch) : null;\n const prs = Array.isArray(msg.prs) ? (msg.prs as VcsPR[]) : [];\n return {\n changes: msg.changes,\n branch,\n prs,\n diffLoading: msg.diffLoading === true,\n };\n },\n});\n\n/** One-off read of the current source-control state. Returns the empty snapshot\n * until the host answers (or if the app lacks `vcs:read`). Use\n * {@link onVcsStateChange} / {@link useVcsState} to react to live updates. */\nexport const getVcsState = (): VcsState => channel.get();\n\n/** Subscribe to source-control changes. Invoked immediately with the current\n * value, then on every host push (diff refresh, PR poll, branch change). Returns\n * an unsubscribe. */\nexport const onVcsStateChange = (listener: (state: VcsState) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook: the current source-control state, re-rendering on every change. */\nexport const useVcsState = (): VcsState => channel.use();\n\n// ---------------------------------------------------------------------------\n// Actions (Recipe B, §8.4). The app NAMES an intent and the HOST performs it —\n// the COW/journal stays in the kernel (§2/§4). `refreshDiff`/`refreshPRs` only\n// cause a host-side recompute + a fresh push (gated `vcs:read`, no new authority);\n// `resetWorkingTree` DISCARDS the user's unsaved work, gated by the first-party-only\n// `vcs:reset` — a fork can never hold it. The arm-then-confirm UX stays in the app;\n// the authority is gated host-side (§8.9), and the host requires `confirm:true` as\n// belt-and-braces (T22).\n// ---------------------------------------------------------------------------\n\n/** An error from a `vcs` action, carrying a machine-readable `.code`. */\nexport interface VcsActionError extends Error {\n code:\n | 'forbidden' // the frame lacks the required capability (`vcs:read` / `vcs:reset`)\n | 'invalid-params' // reset called without `confirm: true`\n | 'no-target' // there is no host contribute session (not in edit mode)\n | 'unknown';\n}\n\ntype VcsResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\nconst vcsRequest = async (method: string, arg: Record<string, unknown> = {}): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_VCS], method, [arg])) as VcsResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `vcs ${method} failed`) as VcsActionError;\n err.code = (res?.code as VcsActionError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/** Ask the host to recompute the working-tree diff and push a fresh {@link VcsState}.\n * Gated `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshDiff = (): Promise<void> => vcsRequest('refreshDiff');\n\n/** Ask the host to re-poll the open PRs and push a fresh {@link VcsState}. Gated\n * `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshPRs = (): Promise<void> => vcsRequest('refreshPRs');\n\n/** Ask the host to DISCARD the working tree (COW writable wipe + journal clear) —\n * irreversible. First-party-only (`vcs:reset`); a fork/preview is refused at the\n * gate. Requires `confirm: true` (host belt-and-braces). Rejects with a\n * {@link VcsActionError} (`.code`). */\nexport const resetWorkingTree = (): Promise<void> => vcsRequest('reset', { confirm: true });\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,yBAAkC;AAClC,0BAAgC;AAChC,sBAA2D;AAC3D,6BAAwB;AA4CxB,MAAM,QAAkB,EAAE,SAAS,CAAC,GAAG,QAAQ,MAAM,KAAK,CAAC,GAAG,aAAa,MAAM;AAEjF,MAAM,gBAAgB,CAAC,MACrB,MAAM,QAAQ,CAAC,KACf,EAAE;AAAA,EACA,CAAC,MACC,CAAC,CAAC,KACF,OAAQ,EAAgB,SAAS,YACjC,OAAQ,EAAgB,WAAW;AACvC;AAEF,MAAM,cAAU,sCAA4B;AAAA,EAC1C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AAGd,QAAI,CAAC,cAAc,IAAI,OAAO,EAAG,QAAO;AACxC,UAAM,SACJ,IAAI,UAAU,OAAO,IAAI,WAAW,WAAY,IAAI,SAAuB;AAC7E,UAAM,MAAM,MAAM,QAAQ,IAAI,GAAG,IAAK,IAAI,MAAkB,CAAC;AAC7D,WAAO;AAAA,MACL,SAAS,IAAI;AAAA,MACb;AAAA,MACA;AAAA,MACA,aAAa,IAAI,gBAAgB;AAAA,IACnC;AAAA,EACF;AACF,CAAC;AAKM,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAKhD,MAAM,mBAAmB,CAAC,aAC/B,QAAQ,SAAS,QAAQ;AAGpB,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAyBvD,MAAM,aAAa,OAAO,QAAgB,MAA+B,CAAC,MAAqB;AAC7F,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,4BAAY,GAAG,QAAQ,CAAC,GAAG,CAAC;AACvE,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,OAAO,MAAM,SAAS;AAC5D,QAAI,OAAQ,KAAK,QAAmC;AACpD,UAAM;AAAA,EACR;AACF;AAIO,MAAM,cAAc,MAAqB,WAAW,aAAa;AAIjE,MAAM,aAAa,MAAqB,WAAW,YAAY;AAM/D,MAAM,mBAAmB,MAAqB,WAAW,SAAS,EAAE,SAAS,KAAK,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/vcs.ts"],"sourcesContent":["// The `vcs:read` / `vcs:reset` source-control surface — app-facing wrappers\n// (UI_AS_APPS_SPEC §5.3 Recipe A read channel + §8.4 Recipe B actions;\n// migrate-sidebars-to-apps Phase 05; roadmap R3-52).\n//\n// A first-party contribute panel (the `panel.contribute` app, Phase 06) reads the\n// live source-control state the native `SourceControlPanel` shows — the working-tree\n// diff summary, the branch lineage, and the open-PR list — and can trigger a\n// refresh/reset. The host derives all of it from authenticated GitHub calls +\n// the COW layers; NONE of the underlying `DiffResult` / `FileSystem` / OAuth token\n// ever crosses the boundary (§8.10) — the wire carries only the plain-JSON\n// {@link VcsState} the host projects.\n//\n// Read side: a Recipe-A push channel, the same get/onChange/use trio as\n// `editorContext` / `diagnostics`. Inert until the host wires the channel\n// (site-main `channelBridge`); an app without `vcs:read` simply sees the empty\n// initial. Action side: `protocol-vcs` requests gated host-side — `refreshDiff` /\n// `refreshPRs` by `vcs:read`, `resetWorkingTree` by first-party-only `vcs:reset`.\nimport { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_VCS, REQUEST_VCS_STATE, VCS_STATE } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** One changed path in the working tree (vs. the loaded ref). `status` mirrors the\n * host `DiffResult` change kinds; `path` is repo-relative. Contents are NOT\n * carried — the native panel showed path + status, and a per-file content diff is\n * a deferred follow-up (plan step 6). */\nexport interface VcsChange {\n path: string;\n status: 'created' | 'modified' | 'deleted';\n}\n\n/** The branch the working tree sits on and the upstream it diverged from\n * (host `BranchInfo`, §15.1). `null` until the user is on a immediately.run-created\n * branch. `upstreamPushable` is `null` while push access is still being probed. */\nexport interface VcsBranch {\n name: string;\n parentRepo: string;\n parentRef: string;\n parentCommitSha: string;\n upstreamPushable: boolean | null;\n}\n\n/** One pull request open from the current branch (host `BranchPR`). */\nexport interface VcsPR {\n number: number;\n url: string;\n title: string;\n state: 'open' | 'closed' | 'merged';\n draft: boolean;\n}\n\n/** The whole source-control snapshot the host projects to a `vcs:read` frame.\n * Plain JSON — never a `DiffResult` / `FileSystem` / `Journal`. */\nexport interface VcsState {\n changes: VcsChange[];\n branch: VcsBranch | null;\n prs: VcsPR[];\n /** True while the host is recomputing the diff — the panel shows a spinner\n * without a separate request (plan gotcha). */\n diffLoading: boolean;\n}\n\n/** Value before the host answers — also the value when the app may not read the\n * channel (no `vcs:read`): an empty, branch-less snapshot. */\nconst EMPTY: VcsState = { changes: [], branch: null, prs: [], diffLoading: false };\n\nconst isChangeArray = (v: unknown): v is VcsChange[] =>\n Array.isArray(v) &&\n v.every((c) => !!c && typeof (c as VcsChange).path === 'string' && typeof (c as VcsChange).status === 'string');\n\nconst channel = createPushChannel<VcsState>({\n pushType: VCS_STATE,\n requestType: REQUEST_VCS_STATE,\n initial: EMPTY,\n parse: (msg) => {\n // Require a well-formed `changes` array; tolerate an absent branch/prs. A\n // malformed push is ignored (returns undefined) so the last good state stands.\n if (!isChangeArray(msg.changes)) return undefined;\n const branch = msg.branch && typeof msg.branch === 'object' ? (msg.branch as VcsBranch) : null;\n const prs = Array.isArray(msg.prs) ? (msg.prs as VcsPR[]) : [];\n return {\n changes: msg.changes,\n branch,\n prs,\n diffLoading: msg.diffLoading === true,\n };\n },\n});\n\n/** One-off read of the current source-control state. Returns the empty snapshot\n * until the host answers (or if the app lacks `vcs:read`). Use\n * {@link onVcsStateChange} / {@link useVcsState} to react to live updates. */\nexport const getVcsState = (): VcsState => channel.get();\n\n/** Subscribe to source-control changes. Invoked immediately with the current\n * value, then on every host push (diff refresh, PR poll, branch change). Returns\n * an unsubscribe. */\nexport const onVcsStateChange = (listener: (state: VcsState) => void): (() => void) => channel.onChange(listener);\n\n/** React hook: the current source-control state, re-rendering on every change. */\nexport const useVcsState = (): VcsState => channel.use();\n\n// ---------------------------------------------------------------------------\n// Actions (Recipe B, §8.4). The app NAMES an intent and the HOST performs it —\n// the COW/journal stays in the kernel (§2/§4). `refreshDiff`/`refreshPRs` only\n// cause a host-side recompute + a fresh push (gated `vcs:read`, no new authority);\n// `resetWorkingTree` DISCARDS the user's unsaved work, gated by the first-party-only\n// `vcs:reset` — a fork can never hold it. The arm-then-confirm UX stays in the app;\n// the authority is gated host-side (§8.9), and the host requires `confirm:true` as\n// belt-and-braces (T22).\n// ---------------------------------------------------------------------------\n\n/** An error from a `vcs` action, carrying a machine-readable `.code`. */\nexport interface VcsActionError extends Error {\n code:\n | 'forbidden' // the frame lacks the required capability (`vcs:read` / `vcs:reset`)\n | 'invalid-params' // reset called without `confirm: true`\n | 'no-target' // there is no host contribute session (not in edit mode)\n | 'unknown';\n}\n\ntype VcsResult = { ok: true; data: unknown } | { ok: false; code: string; message: string };\n\nconst vcsRequest = async (method: string, arg: Record<string, unknown> = {}): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_VCS], method, [arg])) as VcsResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `vcs ${method} failed`) as VcsActionError;\n err.code = (res?.code as VcsActionError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/** Ask the host to recompute the working-tree diff and push a fresh {@link VcsState}.\n * Gated `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshDiff = (): Promise<void> => vcsRequest('refreshDiff');\n\n/** Ask the host to re-poll the open PRs and push a fresh {@link VcsState}. Gated\n * `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshPRs = (): Promise<void> => vcsRequest('refreshPRs');\n\n/** Ask the host to DISCARD the working tree (COW writable wipe + journal clear) —\n * irreversible. First-party-only (`vcs:reset`); a fork/preview is refused at the\n * gate. Requires `confirm: true` (host belt-and-braces). Rejects with a\n * {@link VcsActionError} (`.code`). */\nexport const resetWorkingTree = (): Promise<void> => vcsRequest('reset', { confirm: true });\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,yBAAkC;AAClC,0BAAgC;AAChC,sBAA2D;AAC3D,6BAAwB;AA4CxB,MAAM,QAAkB,EAAE,SAAS,CAAC,GAAG,QAAQ,MAAM,KAAK,CAAC,GAAG,aAAa,MAAM;AAEjF,MAAM,gBAAgB,CAAC,MACrB,MAAM,QAAQ,CAAC,KACf,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,OAAQ,EAAgB,SAAS,YAAY,OAAQ,EAAgB,WAAW,QAAQ;AAEhH,MAAM,cAAU,sCAA4B;AAAA,EAC1C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AAGd,QAAI,CAAC,cAAc,IAAI,OAAO,EAAG,QAAO;AACxC,UAAM,SAAS,IAAI,UAAU,OAAO,IAAI,WAAW,WAAY,IAAI,SAAuB;AAC1F,UAAM,MAAM,MAAM,QAAQ,IAAI,GAAG,IAAK,IAAI,MAAkB,CAAC;AAC7D,WAAO;AAAA,MACL,SAAS,IAAI;AAAA,MACb;AAAA,MACA;AAAA,MACA,aAAa,IAAI,gBAAgB;AAAA,IACnC;AAAA,EACF;AACF,CAAC;AAKM,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAKhD,MAAM,mBAAmB,CAAC,aAAsD,QAAQ,SAAS,QAAQ;AAGzG,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAuBvD,MAAM,aAAa,OAAO,QAAgB,MAA+B,CAAC,MAAqB;AAC7F,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,4BAAY,GAAG,QAAQ,CAAC,GAAG,CAAC;AACvE,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,OAAO,MAAM,SAAS;AAC5D,QAAI,OAAQ,KAAK,QAAmC;AACpD,UAAM;AAAA,EACR;AACF;AAIO,MAAM,cAAc,MAAqB,WAAW,aAAa;AAIjE,MAAM,aAAa,MAAqB,WAAW,YAAY;AAM/D,MAAM,mBAAmB,MAAqB,WAAW,SAAS,EAAE,SAAS,KAAK,CAAC;","names":[]}
package/dist/vcs.js CHANGED
@@ -4,9 +4,7 @@ import { protocolRequest } from "./sandboxUtils";
4
4
  import { PROTOCOL_VCS, REQUEST_VCS_STATE, VCS_STATE } from "./generated/protocol";
5
5
  import { SCHEMES } from "./protocolSchemes";
6
6
  const EMPTY = { changes: [], branch: null, prs: [], diffLoading: false };
7
- const isChangeArray = (v) => Array.isArray(v) && v.every(
8
- (c) => !!c && typeof c.path === "string" && typeof c.status === "string"
9
- );
7
+ const isChangeArray = (v) => Array.isArray(v) && v.every((c) => !!c && typeof c.path === "string" && typeof c.status === "string");
10
8
  const channel = createPushChannel({
11
9
  pushType: VCS_STATE,
12
10
  requestType: REQUEST_VCS_STATE,
package/dist/vcs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/vcs.ts"],"sourcesContent":["// The `vcs:read` / `vcs:reset` source-control surface — app-facing wrappers\n// (UI_AS_APPS_SPEC §5.3 Recipe A read channel + §8.4 Recipe B actions;\n// migrate-sidebars-to-apps Phase 05; roadmap R3-52).\n//\n// A first-party contribute panel (the `panel.contribute` app, Phase 06) reads the\n// live source-control state the native `SourceControlPanel` shows — the working-tree\n// diff summary, the branch lineage, and the open-PR list — and can trigger a\n// refresh/reset. The host derives all of it from authenticated GitHub calls +\n// the COW layers; NONE of the underlying `DiffResult` / `FileSystem` / OAuth token\n// ever crosses the boundary (§8.10) — the wire carries only the plain-JSON\n// {@link VcsState} the host projects.\n//\n// Read side: a Recipe-A push channel, the same get/onChange/use trio as\n// `editorContext` / `diagnostics`. Inert until the host wires the channel\n// (site-main `channelBridge`); an app without `vcs:read` simply sees the empty\n// initial. Action side: `protocol-vcs` requests gated host-side — `refreshDiff` /\n// `refreshPRs` by `vcs:read`, `resetWorkingTree` by first-party-only `vcs:reset`.\nimport { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_VCS, REQUEST_VCS_STATE, VCS_STATE } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** One changed path in the working tree (vs. the loaded ref). `status` mirrors the\n * host `DiffResult` change kinds; `path` is repo-relative. Contents are NOT\n * carried — the native panel showed path + status, and a per-file content diff is\n * a deferred follow-up (plan step 6). */\nexport interface VcsChange {\n path: string;\n status: 'created' | 'modified' | 'deleted';\n}\n\n/** The branch the working tree sits on and the upstream it diverged from\n * (host `BranchInfo`, §15.1). `null` until the user is on a immediately.run-created\n * branch. `upstreamPushable` is `null` while push access is still being probed. */\nexport interface VcsBranch {\n name: string;\n parentRepo: string;\n parentRef: string;\n parentCommitSha: string;\n upstreamPushable: boolean | null;\n}\n\n/** One pull request open from the current branch (host `BranchPR`). */\nexport interface VcsPR {\n number: number;\n url: string;\n title: string;\n state: 'open' | 'closed' | 'merged';\n draft: boolean;\n}\n\n/** The whole source-control snapshot the host projects to a `vcs:read` frame.\n * Plain JSON — never a `DiffResult` / `FileSystem` / `Journal`. */\nexport interface VcsState {\n changes: VcsChange[];\n branch: VcsBranch | null;\n prs: VcsPR[];\n /** True while the host is recomputing the diff — the panel shows a spinner\n * without a separate request (plan gotcha). */\n diffLoading: boolean;\n}\n\n/** Value before the host answers — also the value when the app may not read the\n * channel (no `vcs:read`): an empty, branch-less snapshot. */\nconst EMPTY: VcsState = { changes: [], branch: null, prs: [], diffLoading: false };\n\nconst isChangeArray = (v: unknown): v is VcsChange[] =>\n Array.isArray(v) &&\n v.every(\n (c) =>\n !!c &&\n typeof (c as VcsChange).path === 'string' &&\n typeof (c as VcsChange).status === 'string',\n );\n\nconst channel = createPushChannel<VcsState>({\n pushType: VCS_STATE,\n requestType: REQUEST_VCS_STATE,\n initial: EMPTY,\n parse: (msg) => {\n // Require a well-formed `changes` array; tolerate an absent branch/prs. A\n // malformed push is ignored (returns undefined) so the last good state stands.\n if (!isChangeArray(msg.changes)) return undefined;\n const branch =\n msg.branch && typeof msg.branch === 'object' ? (msg.branch as VcsBranch) : null;\n const prs = Array.isArray(msg.prs) ? (msg.prs as VcsPR[]) : [];\n return {\n changes: msg.changes,\n branch,\n prs,\n diffLoading: msg.diffLoading === true,\n };\n },\n});\n\n/** One-off read of the current source-control state. Returns the empty snapshot\n * until the host answers (or if the app lacks `vcs:read`). Use\n * {@link onVcsStateChange} / {@link useVcsState} to react to live updates. */\nexport const getVcsState = (): VcsState => channel.get();\n\n/** Subscribe to source-control changes. Invoked immediately with the current\n * value, then on every host push (diff refresh, PR poll, branch change). Returns\n * an unsubscribe. */\nexport const onVcsStateChange = (listener: (state: VcsState) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook: the current source-control state, re-rendering on every change. */\nexport const useVcsState = (): VcsState => channel.use();\n\n// ---------------------------------------------------------------------------\n// Actions (Recipe B, §8.4). The app NAMES an intent and the HOST performs it —\n// the COW/journal stays in the kernel (§2/§4). `refreshDiff`/`refreshPRs` only\n// cause a host-side recompute + a fresh push (gated `vcs:read`, no new authority);\n// `resetWorkingTree` DISCARDS the user's unsaved work, gated by the first-party-only\n// `vcs:reset` — a fork can never hold it. The arm-then-confirm UX stays in the app;\n// the authority is gated host-side (§8.9), and the host requires `confirm:true` as\n// belt-and-braces (T22).\n// ---------------------------------------------------------------------------\n\n/** An error from a `vcs` action, carrying a machine-readable `.code`. */\nexport interface VcsActionError extends Error {\n code:\n | 'forbidden' // the frame lacks the required capability (`vcs:read` / `vcs:reset`)\n | 'invalid-params' // reset called without `confirm: true`\n | 'no-target' // there is no host contribute session (not in edit mode)\n | 'unknown';\n}\n\ntype VcsResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\nconst vcsRequest = async (method: string, arg: Record<string, unknown> = {}): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_VCS], method, [arg])) as VcsResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `vcs ${method} failed`) as VcsActionError;\n err.code = (res?.code as VcsActionError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/** Ask the host to recompute the working-tree diff and push a fresh {@link VcsState}.\n * Gated `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshDiff = (): Promise<void> => vcsRequest('refreshDiff');\n\n/** Ask the host to re-poll the open PRs and push a fresh {@link VcsState}. Gated\n * `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshPRs = (): Promise<void> => vcsRequest('refreshPRs');\n\n/** Ask the host to DISCARD the working tree (COW writable wipe + journal clear) —\n * irreversible. First-party-only (`vcs:reset`); a fork/preview is refused at the\n * gate. Requires `confirm: true` (host belt-and-braces). Rejects with a\n * {@link VcsActionError} (`.code`). */\nexport const resetWorkingTree = (): Promise<void> => vcsRequest('reset', { confirm: true });\n"],"mappings":";AAiBA,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAChC,SAAS,cAAc,mBAAmB,iBAAiB;AAC3D,SAAS,eAAe;AA4CxB,MAAM,QAAkB,EAAE,SAAS,CAAC,GAAG,QAAQ,MAAM,KAAK,CAAC,GAAG,aAAa,MAAM;AAEjF,MAAM,gBAAgB,CAAC,MACrB,MAAM,QAAQ,CAAC,KACf,EAAE;AAAA,EACA,CAAC,MACC,CAAC,CAAC,KACF,OAAQ,EAAgB,SAAS,YACjC,OAAQ,EAAgB,WAAW;AACvC;AAEF,MAAM,UAAU,kBAA4B;AAAA,EAC1C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AAGd,QAAI,CAAC,cAAc,IAAI,OAAO,EAAG,QAAO;AACxC,UAAM,SACJ,IAAI,UAAU,OAAO,IAAI,WAAW,WAAY,IAAI,SAAuB;AAC7E,UAAM,MAAM,MAAM,QAAQ,IAAI,GAAG,IAAK,IAAI,MAAkB,CAAC;AAC7D,WAAO;AAAA,MACL,SAAS,IAAI;AAAA,MACb;AAAA,MACA;AAAA,MACA,aAAa,IAAI,gBAAgB;AAAA,IACnC;AAAA,EACF;AACF,CAAC;AAKM,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAKhD,MAAM,mBAAmB,CAAC,aAC/B,QAAQ,SAAS,QAAQ;AAGpB,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAyBvD,MAAM,aAAa,OAAO,QAAgB,MAA+B,CAAC,MAAqB;AAC7F,QAAM,MAAO,MAAM,gBAAgB,QAAQ,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC;AACvE,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,OAAO,MAAM,SAAS;AAC5D,QAAI,OAAQ,KAAK,QAAmC;AACpD,UAAM;AAAA,EACR;AACF;AAIO,MAAM,cAAc,MAAqB,WAAW,aAAa;AAIjE,MAAM,aAAa,MAAqB,WAAW,YAAY;AAM/D,MAAM,mBAAmB,MAAqB,WAAW,SAAS,EAAE,SAAS,KAAK,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/vcs.ts"],"sourcesContent":["// The `vcs:read` / `vcs:reset` source-control surface — app-facing wrappers\n// (UI_AS_APPS_SPEC §5.3 Recipe A read channel + §8.4 Recipe B actions;\n// migrate-sidebars-to-apps Phase 05; roadmap R3-52).\n//\n// A first-party contribute panel (the `panel.contribute` app, Phase 06) reads the\n// live source-control state the native `SourceControlPanel` shows — the working-tree\n// diff summary, the branch lineage, and the open-PR list — and can trigger a\n// refresh/reset. The host derives all of it from authenticated GitHub calls +\n// the COW layers; NONE of the underlying `DiffResult` / `FileSystem` / OAuth token\n// ever crosses the boundary (§8.10) — the wire carries only the plain-JSON\n// {@link VcsState} the host projects.\n//\n// Read side: a Recipe-A push channel, the same get/onChange/use trio as\n// `editorContext` / `diagnostics`. Inert until the host wires the channel\n// (site-main `channelBridge`); an app without `vcs:read` simply sees the empty\n// initial. Action side: `protocol-vcs` requests gated host-side — `refreshDiff` /\n// `refreshPRs` by `vcs:read`, `resetWorkingTree` by first-party-only `vcs:reset`.\nimport { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_VCS, REQUEST_VCS_STATE, VCS_STATE } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** One changed path in the working tree (vs. the loaded ref). `status` mirrors the\n * host `DiffResult` change kinds; `path` is repo-relative. Contents are NOT\n * carried — the native panel showed path + status, and a per-file content diff is\n * a deferred follow-up (plan step 6). */\nexport interface VcsChange {\n path: string;\n status: 'created' | 'modified' | 'deleted';\n}\n\n/** The branch the working tree sits on and the upstream it diverged from\n * (host `BranchInfo`, §15.1). `null` until the user is on a immediately.run-created\n * branch. `upstreamPushable` is `null` while push access is still being probed. */\nexport interface VcsBranch {\n name: string;\n parentRepo: string;\n parentRef: string;\n parentCommitSha: string;\n upstreamPushable: boolean | null;\n}\n\n/** One pull request open from the current branch (host `BranchPR`). */\nexport interface VcsPR {\n number: number;\n url: string;\n title: string;\n state: 'open' | 'closed' | 'merged';\n draft: boolean;\n}\n\n/** The whole source-control snapshot the host projects to a `vcs:read` frame.\n * Plain JSON — never a `DiffResult` / `FileSystem` / `Journal`. */\nexport interface VcsState {\n changes: VcsChange[];\n branch: VcsBranch | null;\n prs: VcsPR[];\n /** True while the host is recomputing the diff — the panel shows a spinner\n * without a separate request (plan gotcha). */\n diffLoading: boolean;\n}\n\n/** Value before the host answers — also the value when the app may not read the\n * channel (no `vcs:read`): an empty, branch-less snapshot. */\nconst EMPTY: VcsState = { changes: [], branch: null, prs: [], diffLoading: false };\n\nconst isChangeArray = (v: unknown): v is VcsChange[] =>\n Array.isArray(v) &&\n v.every((c) => !!c && typeof (c as VcsChange).path === 'string' && typeof (c as VcsChange).status === 'string');\n\nconst channel = createPushChannel<VcsState>({\n pushType: VCS_STATE,\n requestType: REQUEST_VCS_STATE,\n initial: EMPTY,\n parse: (msg) => {\n // Require a well-formed `changes` array; tolerate an absent branch/prs. A\n // malformed push is ignored (returns undefined) so the last good state stands.\n if (!isChangeArray(msg.changes)) return undefined;\n const branch = msg.branch && typeof msg.branch === 'object' ? (msg.branch as VcsBranch) : null;\n const prs = Array.isArray(msg.prs) ? (msg.prs as VcsPR[]) : [];\n return {\n changes: msg.changes,\n branch,\n prs,\n diffLoading: msg.diffLoading === true,\n };\n },\n});\n\n/** One-off read of the current source-control state. Returns the empty snapshot\n * until the host answers (or if the app lacks `vcs:read`). Use\n * {@link onVcsStateChange} / {@link useVcsState} to react to live updates. */\nexport const getVcsState = (): VcsState => channel.get();\n\n/** Subscribe to source-control changes. Invoked immediately with the current\n * value, then on every host push (diff refresh, PR poll, branch change). Returns\n * an unsubscribe. */\nexport const onVcsStateChange = (listener: (state: VcsState) => void): (() => void) => channel.onChange(listener);\n\n/** React hook: the current source-control state, re-rendering on every change. */\nexport const useVcsState = (): VcsState => channel.use();\n\n// ---------------------------------------------------------------------------\n// Actions (Recipe B, §8.4). The app NAMES an intent and the HOST performs it —\n// the COW/journal stays in the kernel (§2/§4). `refreshDiff`/`refreshPRs` only\n// cause a host-side recompute + a fresh push (gated `vcs:read`, no new authority);\n// `resetWorkingTree` DISCARDS the user's unsaved work, gated by the first-party-only\n// `vcs:reset` — a fork can never hold it. The arm-then-confirm UX stays in the app;\n// the authority is gated host-side (§8.9), and the host requires `confirm:true` as\n// belt-and-braces (T22).\n// ---------------------------------------------------------------------------\n\n/** An error from a `vcs` action, carrying a machine-readable `.code`. */\nexport interface VcsActionError extends Error {\n code:\n | 'forbidden' // the frame lacks the required capability (`vcs:read` / `vcs:reset`)\n | 'invalid-params' // reset called without `confirm: true`\n | 'no-target' // there is no host contribute session (not in edit mode)\n | 'unknown';\n}\n\ntype VcsResult = { ok: true; data: unknown } | { ok: false; code: string; message: string };\n\nconst vcsRequest = async (method: string, arg: Record<string, unknown> = {}): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_VCS], method, [arg])) as VcsResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `vcs ${method} failed`) as VcsActionError;\n err.code = (res?.code as VcsActionError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/** Ask the host to recompute the working-tree diff and push a fresh {@link VcsState}.\n * Gated `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshDiff = (): Promise<void> => vcsRequest('refreshDiff');\n\n/** Ask the host to re-poll the open PRs and push a fresh {@link VcsState}. Gated\n * `vcs:read`. Rejects with a {@link VcsActionError} (`.code`). */\nexport const refreshPRs = (): Promise<void> => vcsRequest('refreshPRs');\n\n/** Ask the host to DISCARD the working tree (COW writable wipe + journal clear) —\n * irreversible. First-party-only (`vcs:reset`); a fork/preview is refused at the\n * gate. Requires `confirm: true` (host belt-and-braces). Rejects with a\n * {@link VcsActionError} (`.code`). */\nexport const resetWorkingTree = (): Promise<void> => vcsRequest('reset', { confirm: true });\n"],"mappings":";AAiBA,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAChC,SAAS,cAAc,mBAAmB,iBAAiB;AAC3D,SAAS,eAAe;AA4CxB,MAAM,QAAkB,EAAE,SAAS,CAAC,GAAG,QAAQ,MAAM,KAAK,CAAC,GAAG,aAAa,MAAM;AAEjF,MAAM,gBAAgB,CAAC,MACrB,MAAM,QAAQ,CAAC,KACf,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,OAAQ,EAAgB,SAAS,YAAY,OAAQ,EAAgB,WAAW,QAAQ;AAEhH,MAAM,UAAU,kBAA4B;AAAA,EAC1C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AAGd,QAAI,CAAC,cAAc,IAAI,OAAO,EAAG,QAAO;AACxC,UAAM,SAAS,IAAI,UAAU,OAAO,IAAI,WAAW,WAAY,IAAI,SAAuB;AAC1F,UAAM,MAAM,MAAM,QAAQ,IAAI,GAAG,IAAK,IAAI,MAAkB,CAAC;AAC7D,WAAO;AAAA,MACL,SAAS,IAAI;AAAA,MACb;AAAA,MACA;AAAA,MACA,aAAa,IAAI,gBAAgB;AAAA,IACnC;AAAA,EACF;AACF,CAAC;AAKM,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAKhD,MAAM,mBAAmB,CAAC,aAAsD,QAAQ,SAAS,QAAQ;AAGzG,MAAM,cAAc,MAAgB,QAAQ,IAAI;AAuBvD,MAAM,aAAa,OAAO,QAAgB,MAA+B,CAAC,MAAqB;AAC7F,QAAM,MAAO,MAAM,gBAAgB,QAAQ,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC;AACvE,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,OAAO,MAAM,SAAS;AAC5D,QAAI,OAAQ,KAAK,QAAmC;AACpD,UAAM;AAAA,EACR;AACF;AAIO,MAAM,cAAc,MAAqB,WAAW,aAAa;AAIjE,MAAM,aAAa,MAAqB,WAAW,YAAY;AAM/D,MAAM,mBAAmB,MAAqB,WAAW,SAAS,EAAE,SAAS,KAAK,CAAC;","names":[]}
package/dist/version.cjs CHANGED
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  SDK_VERSION: () => SDK_VERSION
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const SDK_VERSION = "0.45.3";
24
+ const SDK_VERSION = "0.47.0";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  SDK_VERSION
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.45.3';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.47.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.45.3";
2
+ declare const SDK_VERSION = "0.47.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.45.3";
2
+ declare const SDK_VERSION = "0.47.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./chunk-VHAA22YE.js";
2
- const SDK_VERSION = "0.45.3";
2
+ const SDK_VERSION = "0.47.0";
3
3
  export {
4
4
  SDK_VERSION
5
5
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.45.3';\n"],"mappings":";AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.47.0';\n"],"mappings":";AAIO,MAAM,cAAc;","names":[]}