excalidrop 0.1.1

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 (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/dist/cli.d.ts +24 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +533 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/frontend/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
  8. package/dist/frontend/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
  9. package/dist/frontend/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
  10. package/dist/frontend/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
  11. package/dist/frontend/assets/Tableau10-B-NsZVaP.js +1 -0
  12. package/dist/frontend/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
  13. package/dist/frontend/assets/advancedFormat-bKUmsig-.js +1 -0
  14. package/dist/frontend/assets/ar-SA-G6X2FPQ2-C7K_UR3P.js +10 -0
  15. package/dist/frontend/assets/arc-BI_FJWE7.js +1 -0
  16. package/dist/frontend/assets/array-BKyUJesY.js +1 -0
  17. package/dist/frontend/assets/az-AZ-76LH7QW2-PkhuRuSr.js +1 -0
  18. package/dist/frontend/assets/band-dPffDWoQ.js +1 -0
  19. package/dist/frontend/assets/bg-BG-XCXSNQG7-CgLRSYTl.js +5 -0
  20. package/dist/frontend/assets/blockDiagram-38ab4fdb-BFdBRYSn.js +118 -0
  21. package/dist/frontend/assets/blockDiagram-68f4deed-Ynbqr7bQ.js +118 -0
  22. package/dist/frontend/assets/bn-BD-2XOGV67Q-CEvkU0mz.js +5 -0
  23. package/dist/frontend/assets/c4Diagram-15b5d702-Dm7F1Pn_.js +10 -0
  24. package/dist/frontend/assets/c4Diagram-3d4e48cf-COkK-e67.js +10 -0
  25. package/dist/frontend/assets/ca-ES-6MX7JW3Y-DzP6q9bE.js +8 -0
  26. package/dist/frontend/assets/channel-BqCiG5n3.js +1 -0
  27. package/dist/frontend/assets/classDiagram-70f12bd4-CRzulwvH.js +2 -0
  28. package/dist/frontend/assets/classDiagram-d40c83e7-DNbA-n-a.js +2 -0
  29. package/dist/frontend/assets/classDiagram-v2-d5a6b087-l-GglywH.js +2 -0
  30. package/dist/frontend/assets/classDiagram-v2-f2320105-B_TY1zoc.js +2 -0
  31. package/dist/frontend/assets/clone-B6Jqcrku.js +1 -0
  32. package/dist/frontend/assets/createText-2e5e7dd3-DB_k52Pn.js +5 -0
  33. package/dist/frontend/assets/createText-d213de94-D-G49giw.js +5 -0
  34. package/dist/frontend/assets/cs-CZ-2BRQDIVT-Bf4TOzpR.js +11 -0
  35. package/dist/frontend/assets/cytoscape-cose-bilkent-DQYypj0P.js +331 -0
  36. package/dist/frontend/assets/da-DK-5WZEPLOC-jO-5kyXb.js +5 -0
  37. package/dist/frontend/assets/de-DE-XR44H4JA-BkNXWOVh.js +8 -0
  38. package/dist/frontend/assets/directory-open-01563666-DWU9wJ6I.js +1 -0
  39. package/dist/frontend/assets/directory-open-4ed118d0-BzWybGaI.js +1 -0
  40. package/dist/frontend/assets/edges-332bd1c7-CVvMtkTa.js +4 -0
  41. package/dist/frontend/assets/edges-e0da2a9e-DQBAvC8-.js +4 -0
  42. package/dist/frontend/assets/el-GR-BZB4AONW-Dv7Y9hMk.js +10 -0
  43. package/dist/frontend/assets/elk.bundled-YVS3PsMH.js +26 -0
  44. package/dist/frontend/assets/erDiagram-880f2ed8-BjBeR38f.js +51 -0
  45. package/dist/frontend/assets/erDiagram-9861fffd-B2Bi3R45.js +51 -0
  46. package/dist/frontend/assets/es-ES-U4NZUMDT-CWhs0SQY.js +9 -0
  47. package/dist/frontend/assets/eu-ES-A7QVB2H4-BfkFwc3e.js +11 -0
  48. package/dist/frontend/assets/fa-IR-HGAKTJCU-BgWzSj46.js +8 -0
  49. package/dist/frontend/assets/fi-FI-Z5N7JZ37-CUyvkum-.js +6 -0
  50. package/dist/frontend/assets/file-open-002ab408-DIuFHtCF.js +1 -0
  51. package/dist/frontend/assets/file-open-7c801643-684qeFg4.js +1 -0
  52. package/dist/frontend/assets/file-save-3189631c-x92wctJd.js +1 -0
  53. package/dist/frontend/assets/file-save-745eba88-Bb9F9Kg7.js +1 -0
  54. package/dist/frontend/assets/flowDb-7c981674-BSZyyVBf.js +10 -0
  55. package/dist/frontend/assets/flowDb-956e92f1-BC3-CLkF.js +10 -0
  56. package/dist/frontend/assets/flowDiagram-66a62f08-H57DAvmM.js +4 -0
  57. package/dist/frontend/assets/flowDiagram-cbd28bf7-Bw4Z2nBD.js +4 -0
  58. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-CBjbm_oB.js +1 -0
  59. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CoQufPp2.js +1 -0
  60. package/dist/frontend/assets/flowchart-elk-definition-36e2d292-1kZNdf3X.js +114 -0
  61. package/dist/frontend/assets/flowchart-elk-definition-4a651766-DDhG2Esi.js +114 -0
  62. package/dist/frontend/assets/fr-FR-RHASNOE6-7xzoRwbd.js +9 -0
  63. package/dist/frontend/assets/ganttDiagram-04f9e578-CtUSsXCz.js +257 -0
  64. package/dist/frontend/assets/ganttDiagram-c361ad54-K42AklFY.js +257 -0
  65. package/dist/frontend/assets/gitGraphDiagram-21fc4d3e-uW6XA6Y6.js +70 -0
  66. package/dist/frontend/assets/gitGraphDiagram-72cf32ee-CCNbhnBg.js +70 -0
  67. package/dist/frontend/assets/gl-ES-HMX3MZ6V-CXuDDwNP.js +10 -0
  68. package/dist/frontend/assets/graph-CYOmcSYN.js +1 -0
  69. package/dist/frontend/assets/graph-o42dOfI7.js +1 -0
  70. package/dist/frontend/assets/he-IL-6SHJWFNN-O9dzh9EM.js +10 -0
  71. package/dist/frontend/assets/hi-IN-IWLTKZ5I-DlrOXhWs.js +4 -0
  72. package/dist/frontend/assets/hu-HU-A5ZG7DT2-C3eNg-uI.js +7 -0
  73. package/dist/frontend/assets/id-ID-SAP4L64H-6iM4pI5c.js +10 -0
  74. package/dist/frontend/assets/image-blob-reduce.esm-B6b2_-a4.js +7 -0
  75. package/dist/frontend/assets/index-3862675e-DrvLhypC.js +1 -0
  76. package/dist/frontend/assets/index-6079d271-BgVTRqg2.js +1 -0
  77. package/dist/frontend/assets/index-BRA5OktX.css +1 -0
  78. package/dist/frontend/assets/index-D-cr7jpJ.js +349 -0
  79. package/dist/frontend/assets/index-DGmpr33w.js +3 -0
  80. package/dist/frontend/assets/index-HkUXslJy.js +87 -0
  81. package/dist/frontend/assets/infoDiagram-4a4f5b27-ZxmAcaW1.js +7 -0
  82. package/dist/frontend/assets/infoDiagram-f8f76790-Bn-PhiFG.js +7 -0
  83. package/dist/frontend/assets/init-Gi6I4Gst.js +1 -0
  84. package/dist/frontend/assets/it-IT-JPQ66NNP-B26NQzGp.js +11 -0
  85. package/dist/frontend/assets/ja-JP-DBVTYXUO-CB0fvnYJ.js +8 -0
  86. package/dist/frontend/assets/journeyDiagram-29694f62-ggDKs06f.js +139 -0
  87. package/dist/frontend/assets/journeyDiagram-49397b02-pdDS-n-z.js +139 -0
  88. package/dist/frontend/assets/kaa-6HZHGXH3-YNdBdcwJ.js +1 -0
  89. package/dist/frontend/assets/kab-KAB-ZGHBKWFO-D86pwZO0.js +8 -0
  90. package/dist/frontend/assets/katex-ChWnQ-fc.js +261 -0
  91. package/dist/frontend/assets/kk-KZ-P5N5QNE5-CpG5oheM.js +1 -0
  92. package/dist/frontend/assets/km-KH-HSX4SM5Z-DYOZ3Azw.js +11 -0
  93. package/dist/frontend/assets/ko-KR-MTYHY66A-BHorm6lC.js +9 -0
  94. package/dist/frontend/assets/ku-TR-6OUDTVRD-CM9RgEHv.js +9 -0
  95. package/dist/frontend/assets/layout-CCtkxrAf.js +1 -0
  96. package/dist/frontend/assets/layout-DW9Qf-At.js +1 -0
  97. package/dist/frontend/assets/line-DvZfgg9P.js +1 -0
  98. package/dist/frontend/assets/linear-FwFrGRfa.js +1 -0
  99. package/dist/frontend/assets/lt-LT-XHIRWOB4-DvriKXBj.js +3 -0
  100. package/dist/frontend/assets/lv-LV-5QDEKY6T-DzQpwk87.js +7 -0
  101. package/dist/frontend/assets/mindmap-definition-ac74a2e8-DsRo8f2w.js +95 -0
  102. package/dist/frontend/assets/mindmap-definition-fc14e90a-dQ9reluI.js +95 -0
  103. package/dist/frontend/assets/mr-IN-CRQNXWMA-C7RzxxP5.js +13 -0
  104. package/dist/frontend/assets/my-MM-5M5IBNSE-B-RRuy29.js +1 -0
  105. package/dist/frontend/assets/nb-NO-T6EIAALU-B47x16Ik.js +10 -0
  106. package/dist/frontend/assets/nl-NL-IS3SIHDZ-BxAKoQne.js +8 -0
  107. package/dist/frontend/assets/nn-NO-6E72VCQL-dfpbEDSZ.js +8 -0
  108. package/dist/frontend/assets/oc-FR-POXYY2M6-gv6geBjV.js +8 -0
  109. package/dist/frontend/assets/ordinal-Cboi1Yqb.js +1 -0
  110. package/dist/frontend/assets/pa-IN-N4M65BXN-CY30-qH7.js +4 -0
  111. package/dist/frontend/assets/path-CbwjOpE9.js +1 -0
  112. package/dist/frontend/assets/pica-DWYGHLQV.js +7 -0
  113. package/dist/frontend/assets/pie-CDiWh1rj.js +1 -0
  114. package/dist/frontend/assets/pieDiagram-421022e6-Bgy-duyf.js +35 -0
  115. package/dist/frontend/assets/pieDiagram-8a3498a8-DH7FsbPK.js +35 -0
  116. package/dist/frontend/assets/pl-PL-T2D74RX3-TrHugsIA.js +9 -0
  117. package/dist/frontend/assets/pt-BR-5N22H2LF-CJxdFp9c.js +9 -0
  118. package/dist/frontend/assets/pt-PT-UZXXM6DQ-CB-1q6YJ.js +9 -0
  119. package/dist/frontend/assets/quadrantDiagram-0957ecba-DcwYI1oi.js +7 -0
  120. package/dist/frontend/assets/quadrantDiagram-120e2f19-CvHU5a9v.js +7 -0
  121. package/dist/frontend/assets/requirementDiagram-23d650b8-tdagN8HP.js +52 -0
  122. package/dist/frontend/assets/requirementDiagram-deff3bca-BMGM-fSI.js +52 -0
  123. package/dist/frontend/assets/ro-RO-JPDTUUEW-DQq9DBOd.js +11 -0
  124. package/dist/frontend/assets/roundRect-0PYZxl1G.js +1 -0
  125. package/dist/frontend/assets/ru-RU-B4JR7IUQ-CWX32rfh.js +9 -0
  126. package/dist/frontend/assets/sankeyDiagram-04a897e0-3i6zWpY2.js +8 -0
  127. package/dist/frontend/assets/sankeyDiagram-23345273-ncj1Ol7S.js +8 -0
  128. package/dist/frontend/assets/sankeyLinkHorizontal-DgqkLiUE.js +1 -0
  129. package/dist/frontend/assets/selectAll-DJwesbwT.js +1 -0
  130. package/dist/frontend/assets/sequenceDiagram-17ac3bff-ChyKKRZA.js +122 -0
  131. package/dist/frontend/assets/sequenceDiagram-704730f1-D57_wP9k.js +122 -0
  132. package/dist/frontend/assets/si-LK-N5RQ5JYF-Lkc5ZxyG.js +1 -0
  133. package/dist/frontend/assets/sk-SK-C5VTKIMK-DyvYXjqZ.js +6 -0
  134. package/dist/frontend/assets/sl-SI-NN7IZMDC-D_MvP9Rh.js +6 -0
  135. package/dist/frontend/assets/stateDiagram-587899a1-OjFTnFLS.js +1 -0
  136. package/dist/frontend/assets/stateDiagram-9c5f0230-BzU36S5C.js +1 -0
  137. package/dist/frontend/assets/stateDiagram-v2-51a3dcff-CfX1qo7U.js +1 -0
  138. package/dist/frontend/assets/stateDiagram-v2-d93cdb3a-BH7Ud4sU.js +1 -0
  139. package/dist/frontend/assets/styles-2ab5d517-CA8x9Nh5.js +116 -0
  140. package/dist/frontend/assets/styles-5f03d8d2-BnO2pLhV.js +160 -0
  141. package/dist/frontend/assets/styles-6aaf32cf-COnot1ea.js +207 -0
  142. package/dist/frontend/assets/styles-9a916d00-C3QkBgTF.js +160 -0
  143. package/dist/frontend/assets/styles-c10674c1-CPAG2_rr.js +116 -0
  144. package/dist/frontend/assets/styles-edf9a4b0-B8Q9fyu0.js +207 -0
  145. package/dist/frontend/assets/subset-shared.chunk.js +84 -0
  146. package/dist/frontend/assets/subset-worker.chunk.js +1 -0
  147. package/dist/frontend/assets/sv-SE-XGPEYMSR-t6voGQSI.js +10 -0
  148. package/dist/frontend/assets/svgDrawCommon-08f97a94-CdT21LWf.js +1 -0
  149. package/dist/frontend/assets/svgDrawCommon-3ba9043b-D1MU7jAE.js +1 -0
  150. package/dist/frontend/assets/ta-IN-2NMHFXQM-7VmfDDyV.js +9 -0
  151. package/dist/frontend/assets/th-TH-HPSO5L25-B5rLEY9P.js +2 -0
  152. package/dist/frontend/assets/timeline-definition-7e6b55e7-BIcL44O_.js +61 -0
  153. package/dist/frontend/assets/timeline-definition-85554ec2-TgrYYwpU.js +61 -0
  154. package/dist/frontend/assets/tr-TR-DEFEU3FU-0CgOHXTn.js +7 -0
  155. package/dist/frontend/assets/uk-UA-QMV73CPH-CzSFsIsx.js +6 -0
  156. package/dist/frontend/assets/union-CnQAJu0l.js +1 -0
  157. package/dist/frontend/assets/vi-VN-M7AON7JQ-Bhx3rYU9.js +5 -0
  158. package/dist/frontend/assets/xml-BOsq7VnW.js +1 -0
  159. package/dist/frontend/assets/xychartDiagram-b6496bcd-BMKDop6v.js +7 -0
  160. package/dist/frontend/assets/xychartDiagram-e933f94c-DAWithrN.js +7 -0
  161. package/dist/frontend/assets/zh-CN-LNUGB5OW-Br52_wH4.js +10 -0
  162. package/dist/frontend/assets/zh-HK-E62DVLB3-Cj6mnIQ9.js +1 -0
  163. package/dist/frontend/assets/zh-TW-RAJ6MFWO-yLEsVBbC.js +9 -0
  164. package/dist/frontend/assets/zipObject-BeXj5PVd.js +1 -0
  165. package/dist/frontend/index.html +339 -0
  166. package/dist/index.d.ts +4 -0
  167. package/dist/index.d.ts.map +1 -0
  168. package/dist/index.js +2305 -0
  169. package/dist/index.js.map +1 -0
  170. package/dist/remote.d.ts +4 -0
  171. package/dist/remote.d.ts.map +1 -0
  172. package/dist/remote.js +133 -0
  173. package/dist/remote.js.map +1 -0
  174. package/dist/server.d.ts +4 -0
  175. package/dist/server.d.ts.map +1 -0
  176. package/dist/server.js +1217 -0
  177. package/dist/server.js.map +1 -0
  178. package/dist/target.d.ts +41 -0
  179. package/dist/target.d.ts.map +1 -0
  180. package/dist/target.js +244 -0
  181. package/dist/target.js.map +1 -0
  182. package/dist/types.d.ts +228 -0
  183. package/dist/types.d.ts.map +1 -0
  184. package/dist/types.js +53 -0
  185. package/dist/types.js.map +1 -0
  186. package/dist/utils/auth.d.ts +15 -0
  187. package/dist/utils/auth.d.ts.map +1 -0
  188. package/dist/utils/auth.js +68 -0
  189. package/dist/utils/auth.js.map +1 -0
  190. package/dist/utils/github.d.ts +26 -0
  191. package/dist/utils/github.d.ts.map +1 -0
  192. package/dist/utils/github.js +111 -0
  193. package/dist/utils/github.js.map +1 -0
  194. package/dist/utils/logger.d.ts +4 -0
  195. package/dist/utils/logger.d.ts.map +1 -0
  196. package/dist/utils/logger.js +25 -0
  197. package/dist/utils/logger.js.map +1 -0
  198. package/dist/utils/r2store.d.ts +19 -0
  199. package/dist/utils/r2store.d.ts.map +1 -0
  200. package/dist/utils/r2store.js +75 -0
  201. package/dist/utils/r2store.js.map +1 -0
  202. package/frontend/index.html +338 -0
  203. package/frontend/src/App.tsx +1091 -0
  204. package/frontend/src/main.tsx +15 -0
  205. package/frontend/src/utils/ghSync.ts +171 -0
  206. package/frontend/src/utils/mermaidConverter.ts +49 -0
  207. package/frontend/src/utils/webmcp.ts +285 -0
  208. package/package.json +101 -0
  209. package/scripts/check-local-bind.mjs +164 -0
  210. package/scripts/postinstall.mjs +23 -0
  211. package/scripts/publish-pages.sh +39 -0
  212. package/scripts/seed-r2.mjs +9 -0
  213. package/skills/excalidraw-skill/SKILL.md +303 -0
  214. package/skills/excalidraw-skill/evals/evals.json +23 -0
  215. package/skills/excalidraw-skill/references/cheatsheet.md +142 -0
  216. package/skills/excalidraw-skill/scripts/clear-canvas.cjs +38 -0
  217. package/skills/excalidraw-skill/scripts/create-element.cjs +68 -0
  218. package/skills/excalidraw-skill/scripts/delete-element.cjs +48 -0
  219. package/skills/excalidraw-skill/scripts/export-elements.cjs +53 -0
  220. package/skills/excalidraw-skill/scripts/healthcheck.cjs +35 -0
  221. package/skills/excalidraw-skill/scripts/import-elements.cjs +81 -0
  222. package/skills/excalidraw-skill/scripts/update-element.cjs +70 -0
  223. package/src/cli.ts +559 -0
  224. package/src/index.ts +2557 -0
  225. package/src/remote.ts +96 -0
  226. package/src/server.ts +1349 -0
  227. package/src/target.ts +208 -0
  228. package/src/types.ts +338 -0
  229. package/src/utils/auth.ts +61 -0
  230. package/src/utils/github.ts +93 -0
  231. package/src/utils/logger.ts +35 -0
  232. package/src/utils/r2store.ts +76 -0
  233. package/tsconfig.json +50 -0
  234. package/vite.config.js +37 -0
@@ -0,0 +1,93 @@
1
+ /** GitHub as source of truth. Scene = canvas.excalidraw on main; viewer copy on gh-pages. No Actions. */
2
+ import logger from './logger.js';
3
+ import { execSync } from 'node:child_process';
4
+
5
+ const API = 'https://api.github.com';
6
+ /** Zero-config: GITHUB_TOKEN env, else `gh auth token`, else '' (public reads still work). */
7
+ export function token(): string {
8
+ if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
9
+ try {
10
+ return execSync('gh auth token', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
11
+ } catch { return ''; }
12
+ }
13
+ /** Zero-config: current repo from git remote. Empty allowlist = current repo only. */
14
+ export function currentRepo(): string {
15
+ try {
16
+ const url = execSync('git config --get remote.origin.url', { encoding: 'utf8' }).trim();
17
+ const m = url.match(/github\.com[:/]([^/]+\/[^/]+?)(\.git)?$/);
18
+ return (m?.[1] || '').toLowerCase();
19
+ } catch { return ''; }
20
+ }
21
+ export const SCENE_PATH = process.env.SCENE_PATH || 'canvas.excalidraw';
22
+
23
+ export function allowedRepos(): string[] {
24
+ const list = (process.env.ALLOWED_REPOS || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
25
+ if (list.length) return list;
26
+ const cur = currentRepo();
27
+ return cur ? [cur] : [];
28
+ }
29
+ /** Legacy allowlist hook (hosted multi-tenant remote only). Local MCP paths do not
30
+ * gate on it: the user's own token + GitHub's API enforcement is the real
31
+ * gate, and any user must be able to work on any repo they can access. */
32
+ export function assertRepo(_repo: string): void {
33
+ // intentionally no-op — see above
34
+ }
35
+ function headers(): Record<string, string> {
36
+ return { Authorization: `Bearer ${token()}`, Accept: 'application/vnd.github+json', 'User-Agent': 'excalidrop', 'Content-Type': 'application/json' };
37
+ }
38
+ export async function getFile(repo: string, path: string, ref = 'main'): Promise<{ sha: string; content: any } | null> {
39
+ assertRepo(repo);
40
+ const r = await fetch(`${API}/repos/${repo}/contents/${path}?ref=${ref}`, { headers: headers() });
41
+ if (r.status === 404) return null;
42
+ if (!r.ok) throw new Error(`GitHub get ${r.status}: ${await r.text()}`);
43
+ const j = await r.json() as any;
44
+ const text = Buffer.from(j.content, 'base64').toString('utf8');
45
+ return { sha: j.sha, content: JSON.parse(text) };
46
+ }
47
+ export async function putFile(repo: string, path: string, data: any, message: string, branch = 'main', sha?: string): Promise<string> {
48
+ assertRepo(repo);
49
+ if (!token()) throw new Error('GITHUB_TOKEN missing');
50
+ const body: any = { message, content: Buffer.from(JSON.stringify(data, null, 2)).toString('base64'), branch };
51
+ if (sha) body.sha = sha;
52
+ let r = await fetch(`${API}/repos/${repo}/contents/${path}`, { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
53
+ if (r.status === 404 && branch === 'main') {
54
+ // 404 on PUT usually means the branch doesn't exist yet (empty repo —
55
+ // GitHub says plain "Not Found", not "branch missing"). Fork main off
56
+ // gh-pages (or the default branch) then retry.
57
+ const branchExists = await fetch(`${API}/repos/${repo}/git/ref/heads/main`, { headers: headers() });
58
+ if (branchExists.ok) throw new Error(`GitHub put 404: ${await r.text()}`.slice(0, 200));
59
+ const repoInfo = await (await fetch(`${API}/repos/${repo}`, { headers: headers() })).json() as any;
60
+ const from = repoInfo.default_branch && repoInfo.default_branch !== 'main' ? repoInfo.default_branch : 'gh-pages';
61
+ const ref = await (await fetch(`${API}/repos/${repo}/git/ref/heads/${from}`, { headers: headers() })).json() as any;
62
+ if (!ref.object?.sha) throw new Error(`cannot bootstrap main: no ${from} branch`);
63
+ const mkRef = await fetch(`${API}/repos/${repo}/git/refs`, { method: 'POST', headers: headers(), body: JSON.stringify({ ref: 'refs/heads/main', sha: ref.object.sha }) });
64
+ if (!mkRef.ok) throw new Error(`cannot bootstrap main: ${(await mkRef.text()).slice(0, 200)}`);
65
+ // main bootstrapped from another branch may already contain the file — attach its sha
66
+ const existing = await (await fetch(`${API}/repos/${repo}/contents/${path}?ref=main`, { headers: headers() })).json() as any;
67
+ if (existing.sha) body.sha = existing.sha;
68
+ r = await fetch(`${API}/repos/${repo}/contents/${path}`, { method: 'PUT', headers: headers(), body: JSON.stringify(body) });
69
+ }
70
+ if (r.status === 409) throw new Error('CONFLICT: file changed upstream, refetch sha and retry');
71
+ if (!r.ok) throw new Error(`GitHub put ${r.status}: ${await r.text()}`);
72
+ const j = await r.json() as any;
73
+ logger.info(`Committed ${path}@${branch} in ${repo}`);
74
+ return j.content.sha;
75
+ }
76
+ /** Load scene with sha for optimistic concurrency. */
77
+ export async function loadScene(repo: string): Promise<{ elements: any[]; files: any[]; sha: string | null }> {
78
+ assertRepo(repo);
79
+ const f = await getFile(repo, SCENE_PATH, 'main').catch(() => null);
80
+ if (!f) return { elements: [], files: [], sha: null };
81
+ return { elements: f.content.elements || [], files: f.content.files || [], sha: f.sha };
82
+ }
83
+ /** Save scene to main (1 commit per batch). Returns new sha. */
84
+ export async function saveScene(repo: string, elements: any[], files: any[], sha: string | null, msg: string): Promise<string> {
85
+ return putFile(repo, SCENE_PATH, { type: 'excalidraw', version: 2, source: 'excalidrop', elements, files: {} , _files: files }, msg, 'main', sha || undefined);
86
+ }
87
+ /** Sync prebuilt viewer + scene copy straight to gh-pages branch (direct push via Contents API, no Actions). */
88
+ export async function syncPages(repo: string, scene: any): Promise<void> {
89
+ assertRepo(repo);
90
+ // scene copy (viewer reads same-branch file: instant, no raw cache)
91
+ const cur = await getFile(repo, SCENE_PATH, 'gh-pages').catch(() => null);
92
+ await putFile(repo, SCENE_PATH, scene, 'excalidrop: sync scene to gh-pages', 'gh-pages', cur?.sha);
93
+ }
@@ -0,0 +1,35 @@
1
+ import winston from 'winston';
2
+ import { tmpdir } from 'os';
3
+ import { join } from 'path';
4
+
5
+ const LOG_FILE_PATH = process.env.LOG_FILE_PATH || join(tmpdir(), 'excalidraw.log');
6
+
7
+ const logger: winston.Logger = winston.createLogger({
8
+ level: process.env.LOG_LEVEL || 'info',
9
+
10
+ format: winston.format.combine(
11
+ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
12
+ winston.format.uncolorize(),
13
+ winston.format.metadata({ fillExcept: ['message', 'level', 'timestamp'] }),
14
+ winston.format.printf(info => {
15
+ const extra = info.metadata && Object.keys(info.metadata).length
16
+ ? ` ${JSON.stringify(info.metadata)}`
17
+ : '';
18
+ return `${info.timestamp} [${info.level}] ${info.message}${extra}`
19
+ })
20
+ ),
21
+
22
+ transports: [
23
+ new winston.transports.Console({
24
+ level: 'warn', // only warn+error to stderr
25
+ stderrLevels: ['warn','error']
26
+ }),
27
+
28
+ new winston.transports.File({
29
+ filename: LOG_FILE_PATH, // all levels to file
30
+ level: 'debug'
31
+ })
32
+ ]
33
+ });
34
+
35
+ export default logger;
@@ -0,0 +1,76 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ /**
4
+ * Minimal S3-compatible R2 client over fetch with AWS SigV4.
5
+ * Reads creds from env (GH Actions secrets):
6
+ * R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, R2_KEY_PREFIX
7
+ * CLOUDFLARER2TOKEN supported as "accountId:keyId:secret" or "keyId:secret" or raw secret
8
+ * (with R2_ACCOUNT_ID also set) for seamless setup.
9
+ */
10
+ export interface R2Config {
11
+ accountId: string; keyId: string; secret: string; bucket: string; prefix: string; endpoint: string;
12
+ }
13
+
14
+ export function getR2Config(): R2Config | null {
15
+ let accountId = process.env.R2_ACCOUNT_ID || '';
16
+ let keyId = process.env.R2_ACCESS_KEY_ID || '';
17
+ let secret = process.env.R2_SECRET_ACCESS_KEY || '';
18
+ const raw = process.env.CLOUDFLARER2TOKEN || '';
19
+ if ((!keyId || !secret || !accountId) && raw) {
20
+ const parts: string[] = raw.trim().split(':');
21
+ if (parts.length >= 3) { accountId = accountId || (parts[0] as string); keyId = keyId || (parts[1] as string); secret = secret || parts.slice(2).join(':'); }
22
+ else if (parts.length === 2) { keyId = keyId || (parts[0] as string); secret = secret || (parts[1] as string); }
23
+ else { secret = secret || raw.trim(); }
24
+ }
25
+ const bucket = process.env.R2_BUCKET || 'excalidrop';
26
+ const prefix = process.env.R2_KEY_PREFIX || 'scenes/';
27
+ if (!accountId || !keyId || !secret) return null;
28
+ return { accountId, keyId, secret, bucket, prefix, endpoint: `https://${accountId}.r2.cloudflarestorage.com` };
29
+ }
30
+
31
+ async function sha256Hex(data: string | Buffer): Promise<string> {
32
+ return crypto.createHash('sha256').update(data).digest('hex');
33
+ }
34
+ function hmac(key: Buffer | string, data: string): Buffer {
35
+ return crypto.createHmac('sha256', key).update(data).digest();
36
+ }
37
+
38
+ function sign(method: string, urlStr: string, keyId: string, secret: string, region = 'auto', payloadHash?: string, contentType?: string): { Authorization: string; 'x-amz-date': string; 'x-amz-content-sha256': string } {
39
+ const url = new URL(urlStr);
40
+ const now = new Date();
41
+ const amzDate = now.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
42
+ const dateStamp = amzDate.slice(0, 8);
43
+ const hash = payloadHash ?? '';
44
+ const headers: Record<string, string> = { host: url.host, 'x-amz-content-sha256': hash, 'x-amz-date': amzDate };
45
+ if (contentType) headers['content-type'] = contentType;
46
+ const signedHeaders = Object.keys(headers).sort().join(';');
47
+ const canonicalHeaders = Object.keys(headers).sort().map(k => `${k}:${headers[k]}\n`).join('');
48
+ const canonical = [method, url.pathname + (url.search ? '' : ''), url.searchParams.toString(), canonicalHeaders, signedHeaders, hash].join('\n');
49
+ const scope = `${dateStamp}/${region}/s3/aws4_request`;
50
+ const toSign = ['AWS4-HMAC-SHA256', amzDate, scope, crypto.createHash('sha256').update(canonical).digest('hex')].join('\n');
51
+ const kDate = hmac('AWS4' + secret, dateStamp);
52
+ const kRegion = hmac(kDate, region);
53
+ const kService = hmac(kRegion, 's3');
54
+ const kSign = hmac(kService, 'aws4_request');
55
+ const sig = hmac(kSign, toSign).toString('hex');
56
+ return { Authorization: `AWS4-HMAC-SHA256 Credential=${keyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${sig}`, 'x-amz-date': amzDate, 'x-amz-content-sha256': hash };
57
+ }
58
+
59
+ export async function r2Get(cfg: R2Config, key: string): Promise<any | null> {
60
+ const url = `${cfg.endpoint}/${cfg.bucket}/${cfg.prefix}${key}`;
61
+ const empty = await sha256Hex('');
62
+ const s = sign('GET', url, cfg.keyId, cfg.secret, 'auto', empty);
63
+ const r = await fetch(url, { headers: { Authorization: s.Authorization, 'x-amz-date': s['x-amz-date'], 'x-amz-content-sha256': empty } });
64
+ if (r.status === 404 || r.status === 403) return null;
65
+ if (!r.ok) throw new Error(`R2 GET ${r.status}: ${await r.text()}`);
66
+ return r.json();
67
+ }
68
+
69
+ export async function r2Put(cfg: R2Config, key: string, body: any): Promise<void> {
70
+ const data = typeof body === 'string' ? body : JSON.stringify(body);
71
+ const url = `${cfg.endpoint}/${cfg.bucket}/${cfg.prefix}${key}`;
72
+ const hash = await sha256Hex(data);
73
+ const s = sign('PUT', url, cfg.keyId, cfg.secret, 'auto', hash, 'application/json');
74
+ const r = await fetch(url, { method: 'PUT', body: data, headers: { Authorization: s.Authorization, 'x-amz-date': s['x-amz-date'], 'x-amz-content-sha256': hash, 'Content-Type': 'application/json' } });
75
+ if (!r.ok) throw new Error(`R2 PUT ${r.status}: ${await r.text()}`);
76
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "allowJs": false,
7
+ "checkJs": false,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "outDir": "./dist",
12
+ "rootDir": "./src",
13
+ "strict": true,
14
+ "noImplicitAny": true,
15
+ "strictNullChecks": true,
16
+ "strictFunctionTypes": true,
17
+ "strictBindCallApply": true,
18
+ "strictPropertyInitialization": true,
19
+ "noImplicitThis": true,
20
+ "noImplicitReturns": false,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedIndexedAccess": true,
23
+ "noImplicitOverride": true,
24
+ "esModuleInterop": true,
25
+ "allowSyntheticDefaultImports": true,
26
+ "forceConsistentCasingInFileNames": true,
27
+ "skipLibCheck": true,
28
+ "resolveJsonModule": true,
29
+ "isolatedModules": true,
30
+ "noEmitOnError": false,
31
+ "preserveConstEnums": true,
32
+ "removeComments": false,
33
+ "types": ["node"]
34
+ },
35
+ "include": [
36
+ "src/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules",
40
+ "dist",
41
+ "frontend",
42
+ "**/*.test.ts",
43
+ "**/*.spec.ts",
44
+ "**/*.js",
45
+ "**/*.jsx"
46
+ ],
47
+ "ts-node": {
48
+ "esm": true
49
+ }
50
+ }
package/vite.config.js ADDED
@@ -0,0 +1,37 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ root: 'frontend',
6
+ base: process.env.VITE_BASE || './',
7
+ plugins: [react()],
8
+ build: {
9
+ outDir: '../dist/frontend',
10
+ emptyOutDir: true,
11
+ rollupOptions: {
12
+ output: {
13
+ // Excalidraw's font subsetting worker looks for these files by their
14
+ // original (unhashed) names. Preserve them so the 404 doesn't break export.
15
+ chunkFileNames: (chunkInfo) => {
16
+ if (chunkInfo.name.startsWith('subset-')) {
17
+ return 'assets/[name].js'
18
+ }
19
+ return 'assets/[name]-[hash].js'
20
+ },
21
+ },
22
+ },
23
+ },
24
+ server: {
25
+ port: 5173,
26
+ proxy: {
27
+ '/api': {
28
+ target: 'http://127.0.0.1:3000',
29
+ changeOrigin: true,
30
+ },
31
+ '/health': {
32
+ target: 'http://127.0.0.1:3000',
33
+ changeOrigin: true,
34
+ },
35
+ },
36
+ },
37
+ })