excalidrop 0.2.9 → 0.2.11

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 (155) hide show
  1. package/README.md +16 -6
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +78 -28
  5. package/dist/cli.js.map +1 -1
  6. package/dist/frontend/assets/{advancedFormat-DE1yOycD.js → advancedFormat-DZ02N432.js} +1 -1
  7. package/dist/frontend/assets/{ar-SA-G6X2FPQ2-QnaMc7JM.js → ar-SA-G6X2FPQ2-BMsdXV9o.js} +1 -1
  8. package/dist/frontend/assets/{arc-Cg8U0Eq5.js → arc-BmCF6p4G.js} +1 -1
  9. package/dist/frontend/assets/{az-AZ-76LH7QW2-BO9Hr9vy.js → az-AZ-76LH7QW2-6NexfPrP.js} +1 -1
  10. package/dist/frontend/assets/{bg-BG-XCXSNQG7-DsQdG8GQ.js → bg-BG-XCXSNQG7-CPlUaBw3.js} +1 -1
  11. package/dist/frontend/assets/{blockDiagram-38ab4fdb-Cfq7gTl5.js → blockDiagram-38ab4fdb-Df6pwhEY.js} +1 -1
  12. package/dist/frontend/assets/{blockDiagram-68f4deed-DKXvoBM4.js → blockDiagram-68f4deed-BqOBsyzq.js} +1 -1
  13. package/dist/frontend/assets/{bn-BD-2XOGV67Q-0GTwXdnx.js → bn-BD-2XOGV67Q-BP4uTAar.js} +1 -1
  14. package/dist/frontend/assets/{c4Diagram-15b5d702-CzLwW3PM.js → c4Diagram-15b5d702-5cjbBxYL.js} +1 -1
  15. package/dist/frontend/assets/{c4Diagram-3d4e48cf-BtA5sIH_.js → c4Diagram-3d4e48cf-NPGaUxo6.js} +1 -1
  16. package/dist/frontend/assets/{ca-ES-6MX7JW3Y-D792TpuE.js → ca-ES-6MX7JW3Y-DfH9HBoa.js} +1 -1
  17. package/dist/frontend/assets/channel-CLk3EN0K.js +1 -0
  18. package/dist/frontend/assets/{classDiagram-70f12bd4-CYlskbl7.js → classDiagram-70f12bd4-H8kE76DZ.js} +1 -1
  19. package/dist/frontend/assets/{classDiagram-d40c83e7-TFFAmI3n.js → classDiagram-d40c83e7-DL6M651W.js} +1 -1
  20. package/dist/frontend/assets/{classDiagram-v2-d5a6b087-CuEs-vQM.js → classDiagram-v2-d5a6b087-_kNh-4Aj.js} +1 -1
  21. package/dist/frontend/assets/{classDiagram-v2-f2320105-CP2QUPIw.js → classDiagram-v2-f2320105-Kg3gysFv.js} +1 -1
  22. package/dist/frontend/assets/clone-B3pTQIVO.js +1 -0
  23. package/dist/frontend/assets/{createText-2e5e7dd3-B9buhJhz.js → createText-2e5e7dd3-Dv5rgU6Q.js} +1 -1
  24. package/dist/frontend/assets/{createText-d213de94-Dl7cGRq1.js → createText-d213de94-CikBN57T.js} +1 -1
  25. package/dist/frontend/assets/{cs-CZ-2BRQDIVT-BX_XTb-l.js → cs-CZ-2BRQDIVT--kJ2rN0W.js} +1 -1
  26. package/dist/frontend/assets/{cytoscape-cose-bilkent-bdLom2jo.js → cytoscape-cose-bilkent-2pWHV3R0.js} +1 -1
  27. package/dist/frontend/assets/{da-DK-5WZEPLOC-Bfj3iC9L.js → da-DK-5WZEPLOC-B0rRYUX0.js} +1 -1
  28. package/dist/frontend/assets/{de-DE-XR44H4JA-dlBwXmiG.js → de-DE-XR44H4JA-CMf8HU54.js} +1 -1
  29. package/dist/frontend/assets/{edges-332bd1c7-B1wvBqMI.js → edges-332bd1c7-DWrEC7oU.js} +1 -1
  30. package/dist/frontend/assets/{edges-e0da2a9e-CdBHUaUf.js → edges-e0da2a9e-DhXcC1bn.js} +1 -1
  31. package/dist/frontend/assets/{el-GR-BZB4AONW-_x5lVXg8.js → el-GR-BZB4AONW-RnNLXchL.js} +1 -1
  32. package/dist/frontend/assets/{elk.bundled-DtUPYeYN.js → elk.bundled-BdhiY_LN.js} +1 -1
  33. package/dist/frontend/assets/{erDiagram-880f2ed8-BsPsKkXE.js → erDiagram-880f2ed8-CJbWz6ng.js} +1 -1
  34. package/dist/frontend/assets/{erDiagram-9861fffd-Bgpjn4d-.js → erDiagram-9861fffd-CVMtvi1B.js} +1 -1
  35. package/dist/frontend/assets/{es-ES-U4NZUMDT-CgmLkh9-.js → es-ES-U4NZUMDT-Btgh0Ulf.js} +1 -1
  36. package/dist/frontend/assets/{eu-ES-A7QVB2H4-xmyjxW0n.js → eu-ES-A7QVB2H4-L5CrN4JH.js} +1 -1
  37. package/dist/frontend/assets/{fa-IR-HGAKTJCU-BN1XqkA9.js → fa-IR-HGAKTJCU-CnqV75yx.js} +1 -1
  38. package/dist/frontend/assets/{fi-FI-Z5N7JZ37-DPGmsozE.js → fi-FI-Z5N7JZ37-Bg1RKN3A.js} +1 -1
  39. package/dist/frontend/assets/{flowDb-7c981674-D-Lz-ucy.js → flowDb-7c981674-CTKPvbNU.js} +1 -1
  40. package/dist/frontend/assets/{flowDb-956e92f1-CGqQ4oJb.js → flowDb-956e92f1-B6gnLPwZ.js} +1 -1
  41. package/dist/frontend/assets/{flowDiagram-66a62f08-yAsoSO4M.js → flowDiagram-66a62f08-Cf9HBm5b.js} +1 -1
  42. package/dist/frontend/assets/{flowDiagram-cbd28bf7-Cv24oAFw.js → flowDiagram-cbd28bf7-sCRiTzpF.js} +1 -1
  43. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-CUUe93u2.js +1 -0
  44. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-BYBIkWn7.js +1 -0
  45. package/dist/frontend/assets/{flowchart-elk-definition-36e2d292-8RiRHcVG.js → flowchart-elk-definition-36e2d292-fayn7Onc.js} +1 -1
  46. package/dist/frontend/assets/{flowchart-elk-definition-4a651766-Z37NlWB1.js → flowchart-elk-definition-4a651766-BeAj-g_S.js} +1 -1
  47. package/dist/frontend/assets/{fr-FR-RHASNOE6-daCFKfHs.js → fr-FR-RHASNOE6-CSJnO1Nw.js} +1 -1
  48. package/dist/frontend/assets/{ganttDiagram-04f9e578-BMq_3g8O.js → ganttDiagram-04f9e578-C-UwZPsR.js} +1 -1
  49. package/dist/frontend/assets/{ganttDiagram-c361ad54-D09Dl_OA.js → ganttDiagram-c361ad54-DXS6TKMy.js} +1 -1
  50. package/dist/frontend/assets/{gitGraphDiagram-21fc4d3e-ffU3Oe9e.js → gitGraphDiagram-21fc4d3e-BhQ1XZ7r.js} +1 -1
  51. package/dist/frontend/assets/{gitGraphDiagram-72cf32ee-B1qFZhLG.js → gitGraphDiagram-72cf32ee-CcfIRFQe.js} +1 -1
  52. package/dist/frontend/assets/{gl-ES-HMX3MZ6V-Bi_ejBVO.js → gl-ES-HMX3MZ6V-Hdit8WIr.js} +1 -1
  53. package/dist/frontend/assets/{graph-Cld5o79d.js → graph-BoKJ1l0s.js} +1 -1
  54. package/dist/frontend/assets/{graph-D0R7rXy3.js → graph-Cg7-qkBc.js} +1 -1
  55. package/dist/frontend/assets/{he-IL-6SHJWFNN-BxMAYMHO.js → he-IL-6SHJWFNN-CYZ81Ieq.js} +1 -1
  56. package/dist/frontend/assets/{hi-IN-IWLTKZ5I-CEvdaxPD.js → hi-IN-IWLTKZ5I-W-P2k8U3.js} +1 -1
  57. package/dist/frontend/assets/{hu-HU-A5ZG7DT2-DD25K3R6.js → hu-HU-A5ZG7DT2-DLnJ2ysF.js} +1 -1
  58. package/dist/frontend/assets/{id-ID-SAP4L64H-BbNSAn5U.js → id-ID-SAP4L64H-VuiMdNwg.js} +1 -1
  59. package/dist/frontend/assets/{index-3862675e-B37xIMNY.js → index-3862675e-DsNv8Vcf.js} +1 -1
  60. package/dist/frontend/assets/{index-6079d271-MI3jF-v5.js → index-6079d271-Blu14Akr.js} +1 -1
  61. package/dist/frontend/assets/{index-BQuFwudZ.js → index-BNbEa8iL.js} +83 -83
  62. package/dist/frontend/assets/{index-1WYcrLVJ.js → index-CPVapuB0.js} +4 -4
  63. package/dist/frontend/assets/{infoDiagram-4a4f5b27-Ct-SquLn.js → infoDiagram-4a4f5b27-CP06ej38.js} +1 -1
  64. package/dist/frontend/assets/{infoDiagram-f8f76790-DhHJqTvC.js → infoDiagram-f8f76790-DA43b5al.js} +1 -1
  65. package/dist/frontend/assets/{it-IT-JPQ66NNP-DGCSNBVd.js → it-IT-JPQ66NNP-qHIUakPt.js} +1 -1
  66. package/dist/frontend/assets/{ja-JP-DBVTYXUO-BCiZKQS1.js → ja-JP-DBVTYXUO-CsH7mcHn.js} +1 -1
  67. package/dist/frontend/assets/{journeyDiagram-29694f62-JHRAdUlO.js → journeyDiagram-29694f62-CaR9L8pz.js} +1 -1
  68. package/dist/frontend/assets/{journeyDiagram-49397b02-Cz3Xcr6V.js → journeyDiagram-49397b02-wB8iFbnV.js} +1 -1
  69. package/dist/frontend/assets/{kaa-6HZHGXH3-BVSalYYt.js → kaa-6HZHGXH3-lWO0j6KS.js} +1 -1
  70. package/dist/frontend/assets/{kab-KAB-ZGHBKWFO-C9onfV-Y.js → kab-KAB-ZGHBKWFO-kfi8TEEc.js} +1 -1
  71. package/dist/frontend/assets/{kk-KZ-P5N5QNE5-D7HmWmH6.js → kk-KZ-P5N5QNE5-qccfCV7B.js} +1 -1
  72. package/dist/frontend/assets/{km-KH-HSX4SM5Z--gSIK5hM.js → km-KH-HSX4SM5Z-PjPXpzW8.js} +1 -1
  73. package/dist/frontend/assets/{ko-KR-MTYHY66A-DX-ewFiI.js → ko-KR-MTYHY66A-G29lrXhe.js} +1 -1
  74. package/dist/frontend/assets/{ku-TR-6OUDTVRD-BzkN5jDn.js → ku-TR-6OUDTVRD-B2fKd3rp.js} +1 -1
  75. package/dist/frontend/assets/{layout-DTqY_XPF.js → layout-DQLXtLvg.js} +1 -1
  76. package/dist/frontend/assets/{layout-Bg902Usv.js → layout-gkICLqyr.js} +1 -1
  77. package/dist/frontend/assets/{line-CyQqGgtO.js → line-D7qwwgom.js} +1 -1
  78. package/dist/frontend/assets/{linear-Bs8n8jyN.js → linear-BFbIBzfQ.js} +1 -1
  79. package/dist/frontend/assets/{lt-LT-XHIRWOB4-DYMQN62-.js → lt-LT-XHIRWOB4-D9RKVmKS.js} +1 -1
  80. package/dist/frontend/assets/{lv-LV-5QDEKY6T-Ba6KAHxv.js → lv-LV-5QDEKY6T-DQnydFeI.js} +1 -1
  81. package/dist/frontend/assets/{mindmap-definition-ac74a2e8-CFIgjLLb.js → mindmap-definition-ac74a2e8-C-w4pOm4.js} +1 -1
  82. package/dist/frontend/assets/{mindmap-definition-fc14e90a-BrE0Pj3C.js → mindmap-definition-fc14e90a-NAFYTscI.js} +1 -1
  83. package/dist/frontend/assets/{mr-IN-CRQNXWMA-CopS0bND.js → mr-IN-CRQNXWMA-DnvyyL_q.js} +1 -1
  84. package/dist/frontend/assets/{my-MM-5M5IBNSE-CBjLWYW5.js → my-MM-5M5IBNSE-rHP4o4O0.js} +1 -1
  85. package/dist/frontend/assets/{nb-NO-T6EIAALU-CfqYpnNU.js → nb-NO-T6EIAALU-BGndlPdu.js} +1 -1
  86. package/dist/frontend/assets/{nl-NL-IS3SIHDZ-CQao565c.js → nl-NL-IS3SIHDZ-Dnpu3nRl.js} +1 -1
  87. package/dist/frontend/assets/{nn-NO-6E72VCQL-BfcDB6Nm.js → nn-NO-6E72VCQL-DDfQytn1.js} +1 -1
  88. package/dist/frontend/assets/{oc-FR-POXYY2M6-CBEuTLTI.js → oc-FR-POXYY2M6-D5LJqvXX.js} +1 -1
  89. package/dist/frontend/assets/{pa-IN-N4M65BXN-CIRvxv_4.js → pa-IN-N4M65BXN--Kpml_sy.js} +1 -1
  90. package/dist/frontend/assets/{pica-n9a5bdBf.js → pica-CuJIyyZa.js} +1 -1
  91. package/dist/frontend/assets/{pie-Bw6jHLyi.js → pie-B2jLOWNP.js} +1 -1
  92. package/dist/frontend/assets/{pieDiagram-421022e6-Bq7587Ql.js → pieDiagram-421022e6-CisVRxcs.js} +1 -1
  93. package/dist/frontend/assets/{pieDiagram-8a3498a8-Cs30mQ4p.js → pieDiagram-8a3498a8-A0f0pwZX.js} +1 -1
  94. package/dist/frontend/assets/{pl-PL-T2D74RX3-DtyedWZX.js → pl-PL-T2D74RX3-CnSPULBv.js} +1 -1
  95. package/dist/frontend/assets/{pt-BR-5N22H2LF-DN1KIZAy.js → pt-BR-5N22H2LF-BFKmwSAt.js} +1 -1
  96. package/dist/frontend/assets/{pt-PT-UZXXM6DQ-BabgX2Go.js → pt-PT-UZXXM6DQ-0pPefyiR.js} +1 -1
  97. package/dist/frontend/assets/{quadrantDiagram-0957ecba-r08S29Gl.js → quadrantDiagram-0957ecba-CJi_eSba.js} +1 -1
  98. package/dist/frontend/assets/{quadrantDiagram-120e2f19-Cr2SCg_P.js → quadrantDiagram-120e2f19-DSnYz8bj.js} +1 -1
  99. package/dist/frontend/assets/{relay-BgjIEMqw.js → relay-DbGNPZPe.js} +2 -2
  100. package/dist/frontend/assets/{requirementDiagram-23d650b8-DWSUlPnt.js → requirementDiagram-23d650b8-JSmcvLyE.js} +1 -1
  101. package/dist/frontend/assets/{requirementDiagram-deff3bca-D-4X-NA5.js → requirementDiagram-deff3bca-gaD_FhWs.js} +1 -1
  102. package/dist/frontend/assets/{ro-RO-JPDTUUEW-BTtFaTbo.js → ro-RO-JPDTUUEW-DmO8vUup.js} +1 -1
  103. package/dist/frontend/assets/{ru-RU-B4JR7IUQ-Cx7p9fCi.js → ru-RU-B4JR7IUQ-BhCinmor.js} +1 -1
  104. package/dist/frontend/assets/{sankeyDiagram-04a897e0-b2SeMYG6.js → sankeyDiagram-04a897e0-CqODvCJV.js} +1 -1
  105. package/dist/frontend/assets/{sankeyDiagram-23345273-DLciAh1g.js → sankeyDiagram-23345273-DPfCBb02.js} +1 -1
  106. package/dist/frontend/assets/{selectAll-3cbo0gSx.js → selectAll-BLviJ9q8.js} +1 -1
  107. package/dist/frontend/assets/{sequenceDiagram-17ac3bff-D4C97IMR.js → sequenceDiagram-17ac3bff-9Jt1qWsv.js} +1 -1
  108. package/dist/frontend/assets/{sequenceDiagram-704730f1-CsIwqj61.js → sequenceDiagram-704730f1-7Vqzzn5G.js} +1 -1
  109. package/dist/frontend/assets/{si-LK-N5RQ5JYF-DRbkL9Bw.js → si-LK-N5RQ5JYF-ourWdbaN.js} +1 -1
  110. package/dist/frontend/assets/{sk-SK-C5VTKIMK-b8KPqW9j.js → sk-SK-C5VTKIMK-CC6AscVA.js} +1 -1
  111. package/dist/frontend/assets/{sl-SI-NN7IZMDC-DAgg3HEv.js → sl-SI-NN7IZMDC-DtFGvW70.js} +1 -1
  112. package/dist/frontend/assets/{stateDiagram-587899a1-BrL0AJ2m.js → stateDiagram-587899a1-MylHLJIe.js} +1 -1
  113. package/dist/frontend/assets/{stateDiagram-9c5f0230-znRpiySU.js → stateDiagram-9c5f0230-BkHDp6xN.js} +1 -1
  114. package/dist/frontend/assets/{stateDiagram-v2-51a3dcff-Cm0gu2aT.js → stateDiagram-v2-51a3dcff-BILtYz1k.js} +1 -1
  115. package/dist/frontend/assets/{stateDiagram-v2-d93cdb3a-D1v21-Eu.js → stateDiagram-v2-d93cdb3a-B3LJQIzp.js} +1 -1
  116. package/dist/frontend/assets/{styles-2ab5d517-DegF9FpK.js → styles-2ab5d517-DcflbrIB.js} +1 -1
  117. package/dist/frontend/assets/{styles-5f03d8d2-rJg_uZ1y.js → styles-5f03d8d2-F5Nppbak.js} +1 -1
  118. package/dist/frontend/assets/{styles-6aaf32cf-DZ1tE453.js → styles-6aaf32cf-C3L8c7Rh.js} +1 -1
  119. package/dist/frontend/assets/{styles-9a916d00-8MQRBV5u.js → styles-9a916d00-DOdPV1V8.js} +1 -1
  120. package/dist/frontend/assets/{styles-c10674c1-CoYU8Hxx.js → styles-c10674c1-CzbaCJM-.js} +1 -1
  121. package/dist/frontend/assets/{styles-edf9a4b0--FFAlqXa.js → styles-edf9a4b0-DCAhwZki.js} +1 -1
  122. package/dist/frontend/assets/subset-shared.chunk.js +1 -1
  123. package/dist/frontend/assets/subset-worker.chunk.js +1 -1
  124. package/dist/frontend/assets/{sv-SE-XGPEYMSR-BupEJvWR.js → sv-SE-XGPEYMSR-D-yJfeHG.js} +1 -1
  125. package/dist/frontend/assets/{svgDrawCommon-08f97a94-CTj31sRY.js → svgDrawCommon-08f97a94-C424wljT.js} +1 -1
  126. package/dist/frontend/assets/{svgDrawCommon-3ba9043b-BZHRpxTv.js → svgDrawCommon-3ba9043b-B5PRJIhF.js} +1 -1
  127. package/dist/frontend/assets/{ta-IN-2NMHFXQM-DTr9GIu-.js → ta-IN-2NMHFXQM-yQ7tCUpu.js} +1 -1
  128. package/dist/frontend/assets/{th-TH-HPSO5L25-BDPsaOgQ.js → th-TH-HPSO5L25-BEvYXu3g.js} +1 -1
  129. package/dist/frontend/assets/{timeline-definition-7e6b55e7-DYpxvJMC.js → timeline-definition-7e6b55e7-DhpmHpRs.js} +1 -1
  130. package/dist/frontend/assets/{timeline-definition-85554ec2-qV8Ig3TX.js → timeline-definition-85554ec2-BHttd46O.js} +1 -1
  131. package/dist/frontend/assets/{tr-TR-DEFEU3FU-Cgh12nEY.js → tr-TR-DEFEU3FU-Dxe3qE9j.js} +1 -1
  132. package/dist/frontend/assets/{uk-UA-QMV73CPH-DsPGU0Dc.js → uk-UA-QMV73CPH-BHrqCcD7.js} +1 -1
  133. package/dist/frontend/assets/{union-BNBs5eWq.js → union-L_m6MIIH.js} +1 -1
  134. package/dist/frontend/assets/{vi-VN-M7AON7JQ-DCP17qnJ.js → vi-VN-M7AON7JQ-BerIT4e9.js} +1 -1
  135. package/dist/frontend/assets/{xychartDiagram-b6496bcd-BlZqZpsh.js → xychartDiagram-b6496bcd-B32Kkpvb.js} +1 -1
  136. package/dist/frontend/assets/{xychartDiagram-e933f94c-j14yGvbF.js → xychartDiagram-e933f94c-CqC-qq94.js} +1 -1
  137. package/dist/frontend/assets/{zh-CN-LNUGB5OW-DtwvmVxH.js → zh-CN-LNUGB5OW-BoVxD-44.js} +1 -1
  138. package/dist/frontend/assets/{zh-HK-E62DVLB3-ChF_vupN.js → zh-HK-E62DVLB3-CpH3GW8F.js} +1 -1
  139. package/dist/frontend/assets/{zh-TW-RAJ6MFWO-CU8L3VT-.js → zh-TW-RAJ6MFWO-C2QKNRZG.js} +1 -1
  140. package/dist/frontend/assets/{zipObject-BENW7yI5.js → zipObject-BhHnfRS1.js} +1 -1
  141. package/dist/frontend/index.html +18 -1
  142. package/dist/mcp.d.ts.map +1 -1
  143. package/dist/mcp.js +3 -1
  144. package/dist/mcp.js.map +1 -1
  145. package/package.json +1 -1
  146. package/skills/excalidraw-skill/SKILL.md +1 -1
  147. package/worker/src/index.js +11 -18
  148. package/worker/src/mcp.js +8 -1
  149. package/worker/src/oauth.js +233 -0
  150. package/worker/test/oauth.test.mjs +178 -0
  151. package/worker/wrangler.toml +12 -0
  152. package/dist/frontend/assets/channel-BiBGrGc2.js +0 -1
  153. package/dist/frontend/assets/clone-DP5GGGqv.js +0 -1
  154. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-DL-IsgmS.js +0 -1
  155. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-cm0Es-T3.js +0 -1
@@ -0,0 +1,233 @@
1
+ // OAuth Authorization Server for remote MCP clients (spec 2026-07-28,
2
+ // RFC 7591 DCR + RFC 7636 PKCE). The worker fronts the GitHub OAuth App:
3
+ // clients register with US, users log in at github.com, and /token hands
4
+ // back the GitHub user token as the access token. The app secret never
5
+ // leaves the worker. Viewer /exchange flow is untouched.
6
+ //
7
+ // State lives in the OAUTH KV binding (fail closed when missing):
8
+ // oauth:client:{id} {secret, redirect_uris[], name} 90d
9
+ // oauth:session:{sid} {client_id, redirect_uri, scope, challenge, client_state} 10min
10
+ // oauth:code:{code} {client_id, redirect_uri, scope, github_token, challenge} 5min, single-use
11
+
12
+ // App credentials for the AS flow. Prefer dedicated MCP_* vars (your own
13
+ // GitHub OAuth App, where you register /oauth/callback); fall back to the
14
+ // shared viewer-app vars.
15
+ function appCreds(env) {
16
+ return {
17
+ id: env.MCP_GITHUB_CLIENT_ID || env.GITHUB_CLIENT_ID,
18
+ secret: env.MCP_GITHUB_CLIENT_SECRET || env.GITHUB_CLIENT_SECRET,
19
+ };
20
+ }
21
+ // oauth:client:{id} {secret, redirect_uris[], name} 90d
22
+ // oauth:session:{sid} {client_id, redirect_uri, scope, challenge, client_state} 10min
23
+ // oauth:code:{code} {client_id, redirect_uri, scope, github_token, challenge} 5min, single-use
24
+
25
+ function rnd(prefix, bytes = 24) {
26
+ const b = new Uint8Array(bytes);
27
+ crypto.getRandomValues(b);
28
+ let bin = '';
29
+ for (const x of b) bin += String.fromCharCode(x);
30
+ return prefix + btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
31
+ }
32
+
33
+ async function sha256B64url(str) {
34
+ const d = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
35
+ let bin = '';
36
+ for (const x of new Uint8Array(d)) bin += String.fromCharCode(x);
37
+ return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
38
+ }
39
+
40
+ function err(status, error, description, cors) {
41
+ return new Response(JSON.stringify({ error, ...(description ? { error_description: description } : {}) }), {
42
+ status, headers: { 'Content-Type': 'application/json', ...cors },
43
+ });
44
+ }
45
+
46
+ // https anywhere; http only for loopback (RFC 8252 §7.3).
47
+ function redirectAllowed(u) {
48
+ let p;
49
+ try { p = new URL(u); } catch { return false; }
50
+ if (p.protocol === 'https:') return true;
51
+ if (p.protocol === 'http:') return p.hostname === 'localhost' || p.hostname === '127.0.0.1' || p.hostname === '[::1]';
52
+ return false;
53
+ }
54
+
55
+ function clientAuth(req, params) {
56
+ // client_secret_basic or client_secret_post (RFC 6749 §2.3).
57
+ const h = req.headers.get('Authorization') || '';
58
+ const m = /^Basic\s+(.+)$/i.exec(h);
59
+ if (m) {
60
+ try {
61
+ const [id, secret] = atob(m[1]).split(':');
62
+ return { id: id || params.client_id, secret };
63
+ } catch { /* fall through */ }
64
+ }
65
+ return { id: params.client_id, secret: params.client_secret };
66
+ }
67
+
68
+ export async function handleOAuth(req, env, cors) {
69
+ const url = new URL(req.url);
70
+ const origin = url.origin;
71
+ const kv = env.OAUTH;
72
+
73
+ if (req.method === 'GET' && url.pathname === '/.well-known/oauth-authorization-server') {
74
+ return new Response(JSON.stringify({
75
+ issuer: origin,
76
+ authorization_endpoint: origin + '/authorize',
77
+ token_endpoint: origin + '/token',
78
+ registration_endpoint: origin + '/register',
79
+ response_types_supported: ['code'],
80
+ grant_types_supported: ['authorization_code', 'refresh_token'],
81
+ response_modes_supported: ['query'],
82
+ scopes_supported: ['repo'],
83
+ token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post', 'none'],
84
+ code_challenge_methods_supported: ['S256', 'plain'],
85
+ }), { headers: { 'Content-Type': 'application/json', ...cors } });
86
+ }
87
+
88
+ if (req.method === 'GET' && url.pathname === '/.well-known/oauth-protected-resource') {
89
+ return new Response(JSON.stringify({
90
+ resource: origin + '/mcp',
91
+ authorization_servers: [origin],
92
+ scopes_supported: ['repo'],
93
+ bearer_methods_supported: ['header'],
94
+ }), { headers: { 'Content-Type': 'application/json', ...cors } });
95
+ }
96
+
97
+ if (!kv) return err(500, 'server_error', 'oauth store not configured on this deployment', cors);
98
+
99
+ // ---- RFC 7591 Dynamic Client Registration
100
+ if (url.pathname === '/register') {
101
+ if (req.method !== 'POST') return err(405, 'invalid_request', 'use POST', cors);
102
+ let body;
103
+ try { body = await req.json(); } catch { return err(400, 'invalid_request', 'bad json', cors); }
104
+ const uris = body?.redirect_uris;
105
+ if (!Array.isArray(uris) || !uris.length || !uris.every((u) => typeof u === 'string' && redirectAllowed(u))) {
106
+ return err(400, 'invalid_redirect_uri', 'redirect_uris must be non-empty; https required (http allowed for localhost only)', cors);
107
+ }
108
+ const client_id = rnd('excc_');
109
+ const client_secret = rnd('excs_', 32);
110
+ await kv.put(`oauth:client:${client_id}`, JSON.stringify({ secret: client_secret, redirect_uris: uris, name: String(body.client_name || '').slice(0, 100), created: Date.now() }), { expirationTtl: 90 * 86400 });
111
+ return new Response(JSON.stringify({
112
+ client_id, client_secret, redirect_uris: uris,
113
+ grant_types: ['authorization_code', 'refresh_token'],
114
+ token_endpoint_auth_method: 'client_secret_post',
115
+ }), { status: 201, headers: { 'Content-Type': 'application/json', ...cors } });
116
+ }
117
+
118
+ // Where the GitHub leg of the MCP flow returns. Default is the shared
119
+ // central viewer (the one callback URL the shared OAuth App knows); self-host
120
+ // operators point this at their own viewer deployment.
121
+ function viewerCallback(env) {
122
+ return env.OAUTH_VIEWER_CALLBACK || 'https://wtf403.github.io/excalidrop/';
123
+ }
124
+
125
+ // ---- Authorize: validate, stash session, bounce to GitHub
126
+ if (url.pathname === '/authorize') {
127
+ if (req.method !== 'GET') return err(405, 'invalid_request', 'use GET', cors);
128
+ const q = url.searchParams;
129
+ if (q.get('response_type') !== 'code') return err(400, 'unsupported_response_type', 'only code is supported', cors);
130
+ const rawClient = await kv.get(`oauth:client:${q.get('client_id') || ''}`);
131
+ if (!rawClient) return err(400, 'invalid_request', 'unknown client_id (register at POST /register first)', cors);
132
+ const client = JSON.parse(rawClient);
133
+ const redirect_uri = q.get('redirect_uri') || '';
134
+ if (!client.redirect_uris.includes(redirect_uri)) return err(400, 'invalid_request', 'redirect_uri mismatch (must exactly match a registered URI)', cors);
135
+ const app = appCreds(env);
136
+ if (!app.id || !app.secret) return err(500, 'server_error', 'github app not configured', cors);
137
+ const method = q.get('code_challenge_method') || 'plain';
138
+ if (q.get('code_challenge') && method !== 'S256' && method !== 'plain') return err(400, 'invalid_request', 'code_challenge_method must be S256 or plain', cors);
139
+ const sid = rnd('excsess_');
140
+ // GitHub must return the code to a URL registered on the OAuth App. The
141
+ // worker's own /oauth/callback is only registered on apps whose owner
142
+ // added it — otherwise bounce through the registered viewer host, whose
143
+ // bundle forwards ?code&state=excsess_* straight back to /oauth/callback.
144
+ // The exact URI used here is stored and echoed at exchange time.
145
+ const gh_redirect = viewerCallback(env);
146
+ await kv.put(`oauth:session:${sid}`, JSON.stringify({
147
+ client_id: q.get('client_id'), redirect_uri,
148
+ scope: String(q.get('scope') || 'repo').slice(0, 200),
149
+ challenge: q.get('code_challenge') || null, challenge_method: method,
150
+ client_state: q.get('state') || '', gh_redirect,
151
+ }), { expirationTtl: 600 });
152
+ const gh = new URL('https://github.com/login/oauth/authorize');
153
+ gh.searchParams.set('client_id', app.id);
154
+ gh.searchParams.set('redirect_uri', gh_redirect);
155
+ gh.searchParams.set('scope', 'repo');
156
+ gh.searchParams.set('state', sid);
157
+ return new Response(null, { status: 302, headers: { Location: gh.toString(), ...cors } });
158
+ }
159
+
160
+ // ---- Callback from GitHub: swap code, mint our code, return to client
161
+ if (url.pathname === '/oauth/callback') {
162
+ if (req.method !== 'GET') return err(405, 'invalid_request', 'use GET', cors);
163
+ const q = url.searchParams;
164
+ const rawSess = q.get('state') ? await kv.get(`oauth:session:${q.get('state')}`) : null;
165
+ if (!rawSess || !q.get('code')) return err(400, 'invalid_request', 'unknown or expired session (restart login)', cors);
166
+ const sess = JSON.parse(rawSess);
167
+ // redirect_uri must exactly match the authorize leg (stored per session).
168
+ const gh_redirect = sess.gh_redirect || (origin + '/oauth/callback');
169
+ const r = await fetch('https://github.com/login/oauth/access_token', {
170
+ method: 'POST',
171
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
172
+ body: JSON.stringify({
173
+ client_id: appCreds(env).id, client_secret: appCreds(env).secret,
174
+ code: q.get('code'), redirect_uri: gh_redirect,
175
+ }),
176
+ }).catch(() => null);
177
+ const j = await r?.json().catch(() => null);
178
+ if (!r || !r.ok || !j?.access_token) {
179
+ return err(400, 'access_denied', j?.error_description || j?.error || 'github exchange failed', cors);
180
+ }
181
+ const code = rnd('excode_');
182
+ await kv.put(`oauth:code:${code}`, JSON.stringify({
183
+ client_id: sess.client_id, redirect_uri: sess.redirect_uri, scope: 'repo',
184
+ github_token: j.access_token, challenge: sess.challenge, challenge_method: sess.challenge_method,
185
+ }), { expirationTtl: 300 });
186
+ await kv.delete(`oauth:session:${q.get('state')}`);
187
+ const back = new URL(sess.redirect_uri);
188
+ back.searchParams.set('code', code);
189
+ if (sess.client_state) back.searchParams.set('state', sess.client_state);
190
+ return new Response(null, { status: 302, headers: { Location: back.toString(), ...cors } });
191
+ }
192
+
193
+ // ---- Token endpoint
194
+ if (url.pathname === '/token') {
195
+ if (req.method !== 'POST') return err(405, 'invalid_request', 'use POST', cors);
196
+ const ct = req.headers.get('Content-Type') || '';
197
+ let params = {};
198
+ try {
199
+ if (ct.includes('application/json')) params = await req.json();
200
+ else params = Object.fromEntries(new URLSearchParams(await req.text()));
201
+ } catch { return err(400, 'invalid_request', 'bad body', cors); }
202
+ if (params.grant_type === 'refresh_token') {
203
+ return err(400, 'unsupported_grant_type', 'tokens do not expire; re-authorize if needed', cors);
204
+ }
205
+ if (params.grant_type !== 'authorization_code') return err(400, 'unsupported_grant_type', 'only authorization_code is supported', cors);
206
+ const rawCode = params.code ? await kv.get(`oauth:code:${params.code}`) : null;
207
+ if (!rawCode) return err(400, 'invalid_grant', 'unknown or expired code', cors);
208
+ const rec = JSON.parse(rawCode);
209
+ const { id, secret } = clientAuth(req, params);
210
+ if (!id || id !== rec.client_id) return err(401, 'invalid_client', 'client mismatch', cors);
211
+ if (params.redirect_uri && params.redirect_uri !== rec.redirect_uri) return err(400, 'invalid_grant', 'redirect_uri mismatch', cors);
212
+ const rawClient = await kv.get(`oauth:client:${id}`);
213
+ const client = rawClient ? JSON.parse(rawClient) : null;
214
+ if (!client) return err(401, 'invalid_client', 'unknown client', cors);
215
+ if (secret !== undefined && secret !== null && secret !== '') {
216
+ if (secret !== client.secret) return err(401, 'invalid_client', 'bad client_secret', cors);
217
+ } else if (rec.challenge) {
218
+ const verifier = params.code_verifier || '';
219
+ const ok = rec.challenge_method === 'plain'
220
+ ? verifier === rec.challenge
221
+ : verifier && (await sha256B64url(verifier)) === rec.challenge;
222
+ if (!ok) return err(400, 'invalid_grant', 'PKCE verification failed', cors);
223
+ } else {
224
+ return err(401, 'invalid_client', 'client authentication required (secret or PKCE)', cors);
225
+ }
226
+ await kv.delete(`oauth:code:${params.code}`);
227
+ return new Response(JSON.stringify({
228
+ access_token: rec.github_token, token_type: 'Bearer', scope: 'repo',
229
+ }), { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Pragma: 'no-cache', ...cors } });
230
+ }
231
+
232
+ return err(404, 'not_found', 'unknown oauth route', cors);
233
+ }
@@ -0,0 +1,178 @@
1
+ // OAuth Authorization Server contract tests (worker HTTP seam).
2
+ // Run: node worker/test/oauth.test.mjs (fails until src/oauth.js exists)
3
+ import { createHash } from 'node:crypto';
4
+
5
+ const failures = [];
6
+ function check(cond, msg) {
7
+ if (cond) console.log(' ok:', msg);
8
+ else { failures.push(msg); console.log(' FAIL:', msg); }
9
+ }
10
+
11
+ // --- stub env: in-memory KV with TTL, stubbed github fetch
12
+ function makeKv() {
13
+ const m = new Map();
14
+ return {
15
+ async get(k) { const e = m.get(k); if (!e || e.exp < Date.now()) return null; return e.v; },
16
+ async put(k, v, opts = {}) { m.set(k, { v, exp: Date.now() + (opts.expirationTtl || 3600) * 1000 }); },
17
+ async delete(k) { m.delete(k); },
18
+ };
19
+ }
20
+ const GITHUB_CODE = 'gh_code_abc';
21
+ const GITHUB_TOKEN = 'ghu_test123';
22
+ // Records the last exchange body so tests can assert which redirect_uri the
23
+ // worker used for the GitHub leg (must match the authorize leg exactly).
24
+ let lastExchange = null;
25
+ let acceptedSecret = 'TEST_SECRET';
26
+ globalThis.fetch = async (url, init = {}) => {
27
+ const u = String(url);
28
+ if (u === 'https://github.com/login/oauth/access_token') {
29
+ const body = JSON.parse(init.body);
30
+ lastExchange = body;
31
+ if (body.code === GITHUB_CODE && body.client_secret === acceptedSecret) {
32
+ return Response.json({ access_token: GITHUB_TOKEN, scope: 'repo', token_type: 'bearer' });
33
+ }
34
+ return Response.json({ error: 'bad_verification_code' }, { status: 200 });
35
+ }
36
+ throw new Error('unexpected fetch: ' + u);
37
+ };
38
+ const env = { OAUTH: makeKv(), GITHUB_CLIENT_ID: 'TEST_APP_ID', GITHUB_CLIENT_SECRET: 'TEST_SECRET' };
39
+ const HOST = 'https://relay.example.test';
40
+
41
+ const { handleOAuth } = await import('../src/oauth.js');
42
+ const call = (path, opts = {}) => handleOAuth(new Request(HOST + path, opts), env, {});
43
+
44
+ // RFC7636 example pair (independent of implementation)
45
+ const VERIFIER = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';
46
+ const CHALLENGE = createHash('sha256').update(VERIFIER).digest('base64url');
47
+
48
+ // 1. AS metadata points at US, advertises DCR + PKCE
49
+ {
50
+ const r = await call('/.well-known/oauth-authorization-server');
51
+ const j = await r.json();
52
+ check(r.status === 200, 'metadata 200');
53
+ check(j.issuer === HOST, 'metadata issuer is worker origin');
54
+ check(j.authorization_endpoint === HOST + '/authorize', 'metadata authorize endpoint');
55
+ check(j.token_endpoint === HOST + '/token', 'metadata token endpoint');
56
+ check(j.registration_endpoint === HOST + '/register', 'metadata registration endpoint (DCR)');
57
+ check((j.code_challenge_methods_supported || []).includes('S256'), 'metadata advertises S256');
58
+ }
59
+ // 2. protected-resource doc
60
+ {
61
+ const r = await call('/.well-known/oauth-protected-resource');
62
+ const j = await r.json();
63
+ check(r.status === 200, 'protected-resource 200');
64
+ check(j.resource === HOST + '/mcp', 'protected resource is /mcp');
65
+ check((j.authorization_servers || []).includes(HOST), 'protected resource lists our AS');
66
+ }
67
+ // 3. register validation
68
+ {
69
+ const bad = await call('/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ redirect_uris: ['http://evil.com/cb'] }) });
70
+ check(bad.status === 400, 'register rejects non-https redirect');
71
+ const ok = await call('/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ redirect_uris: ['https://app.example/cb'], client_name: 't' }) });
72
+ const j = await ok.json();
73
+ check(ok.status === 201 && !!j.client_id && !!j.client_secret, 'register returns client_id + client_secret');
74
+ var CLIENT = j;
75
+ const local = await call('/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ redirect_uris: ['http://localhost:8787/cb'] }) });
76
+ check(local.status === 201, 'register allows http localhost');
77
+ }
78
+ // 4. authorize validation + github redirect
79
+ {
80
+ const unknown = await call(`/authorize?response_type=code&client_id=nope&redirect_uri=${encodeURIComponent('https://app.example/cb')}`);
81
+ check(unknown.status === 400, 'authorize rejects unknown client');
82
+ const mismatch = await call(`/authorize?response_type=code&client_id=${CLIENT.client_id}&redirect_uri=${encodeURIComponent('https://evil.com/cb')}&state=s1`);
83
+ check(mismatch.status === 400, 'authorize rejects redirect mismatch');
84
+ const ok = await call(`/authorize?response_type=code&client_id=${CLIENT.client_id}&redirect_uri=${encodeURIComponent('https://app.example/cb')}&scope=repo&state=s1&code_challenge=${CHALLENGE}&code_challenge_method=S256`);
85
+ check(ok.status === 302, 'authorize 302s to github');
86
+ const loc = new URL(ok.headers.get('location'));
87
+ check(loc.hostname === 'github.com' && loc.pathname === '/login/oauth/authorize', 'authorize target is github');
88
+ check(loc.searchParams.get('client_id') === 'TEST_APP_ID', 'authorize uses app client_id (secret stays server-side)');
89
+ check(loc.searchParams.get('redirect_uri') === 'https://wtf403.github.io/excalidrop/', 'authorize callback is the registered viewer host (forwarder)');
90
+ check(loc.searchParams.get('scope') === 'repo', 'authorize requests repo scope');
91
+ check(!!loc.searchParams.get('state'), 'authorize carries session state');
92
+ var SESSION = loc.searchParams.get('state');
93
+ }
94
+ // 5. callback: bad state, then happy path
95
+ {
96
+ const bad = await call(`/oauth/callback?code=${GITHUB_CODE}&state=bogus`);
97
+ check(bad.status === 400, 'callback rejects unknown session');
98
+ const ok = await call(`/oauth/callback?code=${GITHUB_CODE}&state=${SESSION}`);
99
+ check(ok.status === 302, 'callback 302s back to client');
100
+ const loc = new URL(ok.headers.get('location'));
101
+ check(loc.origin === 'https://app.example' && loc.pathname === '/cb', 'callback returns to registered redirect');
102
+ check(!!loc.searchParams.get('code') && loc.searchParams.get('state') === 's1', 'callback carries code + client state');
103
+ var CODE = loc.searchParams.get('code');
104
+ }
105
+ // 6. token: bad code, happy path with secret, single-use
106
+ {
107
+ const bad = await call('/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: 'bogus', redirect_uri: 'https://app.example/cb', client_id: CLIENT.client_id, client_secret: CLIENT.client_secret }) });
108
+ check(bad.status === 400 && (await bad.json()).error === 'invalid_grant', 'token rejects bad code');
109
+ const ok = await call('/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: CODE, redirect_uri: 'https://app.example/cb', client_id: CLIENT.client_id, client_secret: CLIENT.client_secret }) });
110
+ const j = await ok.json();
111
+ check(ok.status === 200 && j.access_token === GITHUB_TOKEN && j.token_type === 'Bearer', 'token returns github user token as access_token');
112
+ const reuse = await call('/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: CODE, redirect_uri: 'https://app.example/cb', client_id: CLIENT.client_id, client_secret: CLIENT.client_secret }) });
113
+ check(reuse.status === 400, 'code is single-use');
114
+ }
115
+ // 7. PKCE-only public client (no secret presented)
116
+ {
117
+ const ok = await call(`/authorize?response_type=code&client_id=${CLIENT.client_id}&redirect_uri=${encodeURIComponent('https://app.example/cb')}&state=s2&code_challenge=${CHALLENGE}&code_challenge_method=S256`);
118
+ const sid = new URL(ok.headers.get('location')).searchParams.get('state');
119
+ const cb = await call(`/oauth/callback?code=${GITHUB_CODE}&state=${sid}`);
120
+ const code = new URL(cb.headers.get('location')).searchParams.get('code');
121
+ const good = await call('/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: 'https://app.example/cb', client_id: CLIENT.client_id, code_verifier: VERIFIER }) });
122
+ check(good.status === 200, 'PKCE verifier without secret succeeds');
123
+ const ok2 = await call(`/authorize?response_type=code&client_id=${CLIENT.client_id}&redirect_uri=${encodeURIComponent('https://app.example/cb')}&state=s3&code_challenge=${CHALLENGE}&code_challenge_method=S256`);
124
+ const sid2 = new URL(ok2.headers.get('location')).searchParams.get('state');
125
+ const cb2 = await call(`/oauth/callback?code=${GITHUB_CODE}&state=${sid2}`);
126
+ const code2 = new URL(cb2.headers.get('location')).searchParams.get('code');
127
+ const wrong = await call('/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: code2, redirect_uri: 'https://app.example/cb', client_id: CLIENT.client_id, code_verifier: 'wrong' }) });
128
+ check(wrong.status === 400, 'wrong PKCE verifier rejected');
129
+ }
130
+ // 8. wrong secret rejected
131
+ {
132
+ const ok = await call(`/authorize?response_type=code&client_id=${CLIENT.client_id}&redirect_uri=${encodeURIComponent('https://app.example/cb')}&state=s4&code_challenge=${CHALLENGE}&code_challenge_method=S256`);
133
+ const sid = new URL(ok.headers.get('location')).searchParams.get('state');
134
+ const cb = await call(`/oauth/callback?code=${GITHUB_CODE}&state=${sid}`);
135
+ const code = new URL(cb.headers.get('location')).searchParams.get('code');
136
+ const bad = await call('/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: 'https://app.example/cb', client_id: CLIENT.client_id, client_secret: 'wrong' }) });
137
+ check(bad.status === 401, 'wrong client_secret rejected');
138
+ }
139
+
140
+ // 9. dedicated MCP app credentials override the shared viewer-app ones
141
+ {
142
+ const env2 = { ...env, OAUTH: makeKv(), MCP_GITHUB_CLIENT_ID: 'OWN_APP', MCP_GITHUB_CLIENT_SECRET: 'OWN_SECRET' };
143
+ const reg = await handleOAuth(new Request(HOST + '/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ redirect_uris: ['https://app.example/cb'] }) }), env2, {});
144
+ const cid = (await reg.json()).client_id;
145
+ const auth = await handleOAuth(new Request(HOST + `/authorize?response_type=code&client_id=${cid}&redirect_uri=${encodeURIComponent('https://app.example/cb')}`), env2, {});
146
+ const loc = new URL(auth.headers.get('location'));
147
+ check(loc.searchParams.get('client_id') === 'OWN_APP', 'authorize prefers MCP_GITHUB_CLIENT_ID over shared id');
148
+ }
149
+
150
+ // 10. central-forwarder: GitHub leg uses the registered viewer callback, and
151
+ // the exchange echoes the same redirect_uri (GitHub requires exact match).
152
+ const VIEWER_CB = 'https://viewer.example.test/';
153
+ {
154
+ const env3 = { ...env, OAUTH: makeKv(), OAUTH_VIEWER_CALLBACK: VIEWER_CB };
155
+ const call3 = (path, opts = {}) => handleOAuth(new Request(HOST + path, opts), env3, {});
156
+ const reg = await call3('/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ redirect_uris: ['https://app.example/cb'] }) });
157
+ const cid = (await reg.json()).client_id;
158
+ const auth = await call3(`/authorize?response_type=code&client_id=${cid}&redirect_uri=${encodeURIComponent('https://app.example/cb')}&state=c1`);
159
+ const loc = new URL(auth.headers.get('location'));
160
+ check(loc.searchParams.get('redirect_uri') === VIEWER_CB, 'authorize points GitHub at the viewer callback, not the worker');
161
+ const sid = loc.searchParams.get('state');
162
+ lastExchange = null;
163
+ const cb = await call3(`/oauth/callback?code=${GITHUB_CODE}&state=${sid}`);
164
+ check(cb.status === 302, 'viewer-callback flow completes');
165
+ check(lastExchange && lastExchange.redirect_uri === VIEWER_CB, 'exchange uses the same viewer redirect_uri (exact match)');
166
+ const back = new URL(cb.headers.get('location'));
167
+ check(back.origin === 'https://app.example' && !!back.searchParams.get('code'), 'client still gets its code at its own redirect');
168
+ }
169
+ // 11. default (no OAUTH_VIEWER_CALLBACK): GitHub leg is the shared central
170
+ // viewer (the callback URL the shared app actually has registered).
171
+ {
172
+ const auth = await call(`/authorize?response_type=code&client_id=${CLIENT.client_id}&redirect_uri=${encodeURIComponent('https://app.example/cb')}&state=d1`);
173
+ const loc = new URL(auth.headers.get('location'));
174
+ check(loc.searchParams.get('redirect_uri') === 'https://wtf403.github.io/excalidrop/', 'default GitHub leg is the registered central viewer');
175
+ }
176
+
177
+ console.log(failures.length ? `\n${failures.length} FAILURES` : '\nALL GREEN');
178
+ process.exit(failures.length ? 1 : 0);
@@ -6,6 +6,11 @@ compatibility_date = "2024-01-01"
6
6
  # wrangler secret put GITHUB_CLIENT_SECRET
7
7
  [vars]
8
8
  GITHUB_CLIENT_ID = "Iv23liuS2fx3QOEIoDmx"
9
+ # Viewer host the MCP OAuth flow bounces through. Must be registered as an
10
+ # Authorization callback URL on the GitHub OAuth App (default: the shared
11
+ # central viewer). Self-host operators serving their own viewer set this to
12
+ # their viewer origin (trailing slash) instead of registering the worker.
13
+ OAUTH_VIEWER_CALLBACK = "https://wtf403.github.io/excalidrop/"
9
14
  # Comma-separated allowed canvas origins: exact ("https://wtf403.github.io")
10
15
  # or "*.github.io"-style suffixes so one deployment serves every publisher.
11
16
  # Add "*.pages.dev" + your custom domain when hosting private viewers on Cloudflare.
@@ -15,6 +20,13 @@ ALLOWED_REDIRECT_ORIGINS = "*.github.io,*.pages.dev"
15
20
  name = "RELAY"
16
21
  class_name = "RelayRoom"
17
22
 
23
+ # OAuth Authorization Server store (DCR clients, login sessions, one-time
24
+ # codes — all short-TTL). Absent on self-host deploys → oauth routes 500,
25
+ # relay + /mcp with static tokens keep working.
26
+ [[kv_namespaces]]
27
+ binding = "OAUTH"
28
+ id = "f956647497954a68a84d1c275d3b9343"
29
+
18
30
  [[migrations]]
19
31
  tag = "v1"
20
32
  new_sqlite_classes = ["RelayRoom"]
@@ -1 +0,0 @@
1
- import{U as a,E as n}from"./index-BQuFwudZ.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{b as r}from"./union-BNBs5eWq.js";var e=4;function a(o){return r(o,e)}export{a as c};
@@ -1 +0,0 @@
1
- import{f as o,p as e}from"./flowDb-956e92f1-CGqQ4oJb.js";import{f as a,a as t}from"./styles-c10674c1-CoYU8Hxx.js";import{t as i}from"./index-1WYcrLVJ.js";import"./index-BQuFwudZ.js";import"./graph-D0R7rXy3.js";import"./layout-DTqY_XPF.js";import"./index-3862675e-B37xIMNY.js";import"./union-BNBs5eWq.js";import"./clone-DP5GGGqv.js";import"./zipObject-BENW7yI5.js";import"./edges-e0da2a9e-CdBHUaUf.js";import"./createText-2e5e7dd3-B9buhJhz.js";import"./index-DGmpr33w.js";import"./line-CyQqGgtO.js";import"./array-BKyUJesY.js";import"./path-CbwjOpE9.js";import"./channel-BiBGrGc2.js";import"./selectAll-3cbo0gSx.js";const x={parser:e,db:o,renderer:t,styles:a,init:r=>{r.flowchart||(r.flowchart={}),r.flowchart.arrowMarkerAbsolute=r.arrowMarkerAbsolute,i({flowchart:{arrowMarkerAbsolute:r.arrowMarkerAbsolute}}),t.setConf(r.flowchart),o.clear(),o.setGen("gen-2")}};export{x as diagram};
@@ -1 +0,0 @@
1
- import{f as o,p as e}from"./flowDb-7c981674-D-Lz-ucy.js";import{f as a,a as t}from"./styles-2ab5d517-DegF9FpK.js";import{K as i}from"./index-BQuFwudZ.js";import"./graph-Cld5o79d.js";import"./layout-Bg902Usv.js";import"./index-6079d271-MI3jF-v5.js";import"./union-BNBs5eWq.js";import"./clone-DP5GGGqv.js";import"./zipObject-BENW7yI5.js";import"./edges-332bd1c7-B1wvBqMI.js";import"./createText-d213de94-Dl7cGRq1.js";import"./index-DGmpr33w.js";import"./line-CyQqGgtO.js";import"./array-BKyUJesY.js";import"./path-CbwjOpE9.js";import"./channel-BiBGrGc2.js";import"./selectAll-3cbo0gSx.js";const C={parser:e,db:o,renderer:t,styles:a,init:r=>{r.flowchart||(r.flowchart={}),r.flowchart.arrowMarkerAbsolute=r.arrowMarkerAbsolute,i({flowchart:{arrowMarkerAbsolute:r.arrowMarkerAbsolute}}),t.setConf(r.flowchart),o.clear(),o.setGen("gen-2")}};export{C as diagram};