@knpkv/confluence-to-markdown 0.2.0 → 0.4.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 (336) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +282 -14
  4. package/dist/ConfluenceAuth.d.ts +76 -0
  5. package/dist/ConfluenceAuth.d.ts.map +1 -0
  6. package/dist/ConfluenceAuth.js +356 -0
  7. package/dist/ConfluenceAuth.js.map +1 -0
  8. package/dist/ConfluenceClient.d.ts +26 -2
  9. package/dist/ConfluenceClient.d.ts.map +1 -1
  10. package/dist/ConfluenceClient.js +98 -92
  11. package/dist/ConfluenceClient.js.map +1 -1
  12. package/dist/ConfluenceConfig.d.ts +4 -24
  13. package/dist/ConfluenceConfig.d.ts.map +1 -1
  14. package/dist/ConfluenceConfig.js +45 -7
  15. package/dist/ConfluenceConfig.js.map +1 -1
  16. package/dist/ConfluenceError.d.ts +89 -6
  17. package/dist/ConfluenceError.d.ts.map +1 -1
  18. package/dist/ConfluenceError.js +88 -5
  19. package/dist/ConfluenceError.js.map +1 -1
  20. package/dist/GitError.d.ts +103 -0
  21. package/dist/GitError.d.ts.map +1 -0
  22. package/dist/GitError.js +85 -0
  23. package/dist/GitError.js.map +1 -0
  24. package/dist/GitService.d.ts +175 -0
  25. package/dist/GitService.d.ts.map +1 -0
  26. package/dist/GitService.js +431 -0
  27. package/dist/GitService.js.map +1 -0
  28. package/dist/LocalFileSystem.d.ts +29 -4
  29. package/dist/LocalFileSystem.d.ts.map +1 -1
  30. package/dist/LocalFileSystem.js +80 -6
  31. package/dist/LocalFileSystem.js.map +1 -1
  32. package/dist/MarkdownConverter.d.ts +49 -2
  33. package/dist/MarkdownConverter.d.ts.map +1 -1
  34. package/dist/MarkdownConverter.js +73 -111
  35. package/dist/MarkdownConverter.js.map +1 -1
  36. package/dist/SchemaConverterError.d.ts +108 -0
  37. package/dist/SchemaConverterError.d.ts.map +1 -0
  38. package/dist/SchemaConverterError.js +84 -0
  39. package/dist/SchemaConverterError.js.map +1 -0
  40. package/dist/Schemas.d.ts +225 -1
  41. package/dist/Schemas.d.ts.map +1 -1
  42. package/dist/Schemas.js +155 -6
  43. package/dist/Schemas.js.map +1 -1
  44. package/dist/SyncEngine.d.ts +30 -20
  45. package/dist/SyncEngine.d.ts.map +1 -1
  46. package/dist/SyncEngine.js +566 -117
  47. package/dist/SyncEngine.js.map +1 -1
  48. package/dist/ast/BlockNode.d.ts +468 -0
  49. package/dist/ast/BlockNode.d.ts.map +1 -0
  50. package/dist/ast/BlockNode.js +319 -0
  51. package/dist/ast/BlockNode.js.map +1 -0
  52. package/dist/ast/Document.d.ts +244 -0
  53. package/dist/ast/Document.d.ts.map +1 -0
  54. package/dist/ast/Document.js +69 -0
  55. package/dist/ast/Document.js.map +1 -0
  56. package/dist/ast/InlineNode.d.ts +477 -0
  57. package/dist/ast/InlineNode.d.ts.map +1 -0
  58. package/dist/ast/InlineNode.js +263 -0
  59. package/dist/ast/InlineNode.js.map +1 -0
  60. package/dist/ast/MacroNode.d.ts +267 -0
  61. package/dist/ast/MacroNode.d.ts.map +1 -0
  62. package/dist/ast/MacroNode.js +164 -0
  63. package/dist/ast/MacroNode.js.map +1 -0
  64. package/dist/ast/index.d.ts +10 -0
  65. package/dist/ast/index.d.ts.map +1 -0
  66. package/dist/ast/index.js +14 -0
  67. package/dist/ast/index.js.map +1 -0
  68. package/dist/bin.js +33 -149
  69. package/dist/bin.js.map +1 -1
  70. package/dist/commands/auth.d.ts +15 -0
  71. package/dist/commands/auth.d.ts.map +1 -0
  72. package/dist/commands/auth.js +86 -0
  73. package/dist/commands/auth.js.map +1 -0
  74. package/dist/commands/clone.d.ts +12 -0
  75. package/dist/commands/clone.d.ts.map +1 -0
  76. package/dist/commands/clone.js +93 -0
  77. package/dist/commands/clone.js.map +1 -0
  78. package/dist/commands/delete.d.ts +13 -0
  79. package/dist/commands/delete.d.ts.map +1 -0
  80. package/dist/commands/delete.js +48 -0
  81. package/dist/commands/delete.js.map +1 -0
  82. package/dist/commands/errorHandler.d.ts +14 -0
  83. package/dist/commands/errorHandler.d.ts.map +1 -0
  84. package/dist/commands/errorHandler.js +33 -0
  85. package/dist/commands/errorHandler.js.map +1 -0
  86. package/dist/commands/git.d.ts +22 -0
  87. package/dist/commands/git.d.ts.map +1 -0
  88. package/dist/commands/git.js +72 -0
  89. package/dist/commands/git.js.map +1 -0
  90. package/dist/commands/index.d.ts +11 -0
  91. package/dist/commands/index.d.ts.map +1 -0
  92. package/dist/commands/index.js +11 -0
  93. package/dist/commands/index.js.map +1 -0
  94. package/dist/commands/layers.d.ts +31 -0
  95. package/dist/commands/layers.d.ts.map +1 -0
  96. package/dist/commands/layers.js +137 -0
  97. package/dist/commands/layers.js.map +1 -0
  98. package/dist/commands/new.d.ts +9 -0
  99. package/dist/commands/new.d.ts.map +1 -0
  100. package/dist/commands/new.js +80 -0
  101. package/dist/commands/new.js.map +1 -0
  102. package/dist/commands/pageTree.d.ts +18 -0
  103. package/dist/commands/pageTree.d.ts.map +1 -0
  104. package/dist/commands/pageTree.js +20 -0
  105. package/dist/commands/pageTree.js.map +1 -0
  106. package/dist/commands/shared.d.ts +15 -0
  107. package/dist/commands/shared.d.ts.map +1 -0
  108. package/dist/commands/shared.js +27 -0
  109. package/dist/commands/shared.js.map +1 -0
  110. package/dist/commands/sync.d.ts +15 -0
  111. package/dist/commands/sync.d.ts.map +1 -0
  112. package/dist/commands/sync.js +101 -0
  113. package/dist/commands/sync.js.map +1 -0
  114. package/dist/index.d.ts +10 -1
  115. package/dist/index.d.ts.map +1 -1
  116. package/dist/index.js +14 -0
  117. package/dist/index.js.map +1 -1
  118. package/dist/internal/NodeLayers.d.ts +7 -0
  119. package/dist/internal/NodeLayers.d.ts.map +1 -0
  120. package/dist/internal/NodeLayers.js +19 -0
  121. package/dist/internal/NodeLayers.js.map +1 -0
  122. package/dist/internal/frontmatter.d.ts +10 -0
  123. package/dist/internal/frontmatter.d.ts.map +1 -1
  124. package/dist/internal/frontmatter.js +16 -0
  125. package/dist/internal/frontmatter.js.map +1 -1
  126. package/dist/internal/gitCommands.d.ts +78 -0
  127. package/dist/internal/gitCommands.d.ts.map +1 -0
  128. package/dist/internal/gitCommands.js +156 -0
  129. package/dist/internal/gitCommands.js.map +1 -0
  130. package/dist/internal/hashUtils.d.ts +42 -1
  131. package/dist/internal/hashUtils.d.ts.map +1 -1
  132. package/dist/internal/hashUtils.js +38 -2
  133. package/dist/internal/hashUtils.js.map +1 -1
  134. package/dist/internal/oauthServer.d.ts +55 -0
  135. package/dist/internal/oauthServer.d.ts.map +1 -0
  136. package/dist/internal/oauthServer.js +110 -0
  137. package/dist/internal/oauthServer.js.map +1 -0
  138. package/dist/internal/pathUtils.d.ts +21 -4
  139. package/dist/internal/pathUtils.d.ts.map +1 -1
  140. package/dist/internal/pathUtils.js +24 -13
  141. package/dist/internal/pathUtils.js.map +1 -1
  142. package/dist/internal/tokenStorage.d.ts +75 -0
  143. package/dist/internal/tokenStorage.d.ts.map +1 -0
  144. package/dist/internal/tokenStorage.js +149 -0
  145. package/dist/internal/tokenStorage.js.map +1 -0
  146. package/dist/internal/userCache.d.ts +42 -0
  147. package/dist/internal/userCache.d.ts.map +1 -0
  148. package/dist/internal/userCache.js +51 -0
  149. package/dist/internal/userCache.js.map +1 -0
  150. package/dist/parsers/ConfluenceParser.d.ts +26 -0
  151. package/dist/parsers/ConfluenceParser.d.ts.map +1 -0
  152. package/dist/parsers/ConfluenceParser.js +792 -0
  153. package/dist/parsers/ConfluenceParser.js.map +1 -0
  154. package/dist/parsers/MarkdownParser.d.ts +26 -0
  155. package/dist/parsers/MarkdownParser.d.ts.map +1 -0
  156. package/dist/parsers/MarkdownParser.js +873 -0
  157. package/dist/parsers/MarkdownParser.js.map +1 -0
  158. package/dist/parsers/index.d.ts +8 -0
  159. package/dist/parsers/index.d.ts.map +1 -0
  160. package/dist/parsers/index.js +8 -0
  161. package/dist/parsers/index.js.map +1 -0
  162. package/dist/schemas/ConfluenceSchema.d.ts +21 -0
  163. package/dist/schemas/ConfluenceSchema.d.ts.map +1 -0
  164. package/dist/schemas/ConfluenceSchema.js +38 -0
  165. package/dist/schemas/ConfluenceSchema.js.map +1 -0
  166. package/dist/schemas/ConversionSchema.d.ts +35 -0
  167. package/dist/schemas/ConversionSchema.d.ts.map +1 -0
  168. package/dist/schemas/ConversionSchema.js +208 -0
  169. package/dist/schemas/ConversionSchema.js.map +1 -0
  170. package/dist/schemas/MarkdownSchema.d.ts +21 -0
  171. package/dist/schemas/MarkdownSchema.d.ts.map +1 -0
  172. package/dist/schemas/MarkdownSchema.js +38 -0
  173. package/dist/schemas/MarkdownSchema.js.map +1 -0
  174. package/dist/schemas/hast/HastFromHtml.d.ts +27 -0
  175. package/dist/schemas/hast/HastFromHtml.d.ts.map +1 -0
  176. package/dist/schemas/hast/HastFromHtml.js +107 -0
  177. package/dist/schemas/hast/HastFromHtml.js.map +1 -0
  178. package/dist/schemas/hast/HastSchema.d.ts +195 -0
  179. package/dist/schemas/hast/HastSchema.d.ts.map +1 -0
  180. package/dist/schemas/hast/HastSchema.js +183 -0
  181. package/dist/schemas/hast/HastSchema.js.map +1 -0
  182. package/dist/schemas/hast/index.d.ts +9 -0
  183. package/dist/schemas/hast/index.d.ts.map +1 -0
  184. package/dist/schemas/hast/index.js +3 -0
  185. package/dist/schemas/hast/index.js.map +1 -0
  186. package/dist/schemas/index.d.ts +14 -0
  187. package/dist/schemas/index.d.ts.map +1 -0
  188. package/dist/schemas/index.js +16 -0
  189. package/dist/schemas/index.js.map +1 -0
  190. package/dist/schemas/mdast/MdastFromMarkdown.d.ts +30 -0
  191. package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +1 -0
  192. package/dist/schemas/mdast/MdastFromMarkdown.js +79 -0
  193. package/dist/schemas/mdast/MdastFromMarkdown.js.map +1 -0
  194. package/dist/schemas/mdast/MdastSchema.d.ts +385 -0
  195. package/dist/schemas/mdast/MdastSchema.d.ts.map +1 -0
  196. package/dist/schemas/mdast/MdastSchema.js +266 -0
  197. package/dist/schemas/mdast/MdastSchema.js.map +1 -0
  198. package/dist/schemas/mdast/index.d.ts +10 -0
  199. package/dist/schemas/mdast/index.d.ts.map +1 -0
  200. package/dist/schemas/mdast/index.js +4 -0
  201. package/dist/schemas/mdast/index.js.map +1 -0
  202. package/dist/schemas/mdast/mdastToString.d.ts +13 -0
  203. package/dist/schemas/mdast/mdastToString.d.ts.map +1 -0
  204. package/dist/schemas/mdast/mdastToString.js +85 -0
  205. package/dist/schemas/mdast/mdastToString.js.map +1 -0
  206. package/dist/schemas/nodes/block/BlockSchema.d.ts +43 -0
  207. package/dist/schemas/nodes/block/BlockSchema.d.ts.map +1 -0
  208. package/dist/schemas/nodes/block/BlockSchema.js +634 -0
  209. package/dist/schemas/nodes/block/BlockSchema.js.map +1 -0
  210. package/dist/schemas/nodes/block/index.d.ts +7 -0
  211. package/dist/schemas/nodes/block/index.d.ts.map +1 -0
  212. package/dist/schemas/nodes/block/index.js +7 -0
  213. package/dist/schemas/nodes/block/index.js.map +1 -0
  214. package/dist/schemas/nodes/index.d.ts +9 -0
  215. package/dist/schemas/nodes/index.d.ts.map +1 -0
  216. package/dist/schemas/nodes/index.js +12 -0
  217. package/dist/schemas/nodes/index.js.map +1 -0
  218. package/dist/schemas/nodes/inline/InlineSchema.d.ts +48 -0
  219. package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +1 -0
  220. package/dist/schemas/nodes/inline/InlineSchema.js +436 -0
  221. package/dist/schemas/nodes/inline/InlineSchema.js.map +1 -0
  222. package/dist/schemas/nodes/inline/index.d.ts +7 -0
  223. package/dist/schemas/nodes/inline/index.d.ts.map +1 -0
  224. package/dist/schemas/nodes/inline/index.js +7 -0
  225. package/dist/schemas/nodes/inline/index.js.map +1 -0
  226. package/dist/schemas/nodes/macro/MacroSchema.d.ts +27 -0
  227. package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +1 -0
  228. package/dist/schemas/nodes/macro/MacroSchema.js +162 -0
  229. package/dist/schemas/nodes/macro/MacroSchema.js.map +1 -0
  230. package/dist/schemas/nodes/macro/index.d.ts +7 -0
  231. package/dist/schemas/nodes/macro/index.d.ts.map +1 -0
  232. package/dist/schemas/nodes/macro/index.js +7 -0
  233. package/dist/schemas/nodes/macro/index.js.map +1 -0
  234. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +24 -0
  235. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +1 -0
  236. package/dist/schemas/preprocessing/ConfluencePreprocessor.js +351 -0
  237. package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +1 -0
  238. package/dist/schemas/preprocessing/index.d.ts +8 -0
  239. package/dist/schemas/preprocessing/index.d.ts.map +1 -0
  240. package/dist/schemas/preprocessing/index.js +2 -0
  241. package/dist/schemas/preprocessing/index.js.map +1 -0
  242. package/dist/serializers/ConfluenceSerializer.d.ts +30 -0
  243. package/dist/serializers/ConfluenceSerializer.d.ts.map +1 -0
  244. package/dist/serializers/ConfluenceSerializer.js +551 -0
  245. package/dist/serializers/ConfluenceSerializer.js.map +1 -0
  246. package/dist/serializers/MarkdownSerializer.d.ts +34 -0
  247. package/dist/serializers/MarkdownSerializer.d.ts.map +1 -0
  248. package/dist/serializers/MarkdownSerializer.js +355 -0
  249. package/dist/serializers/MarkdownSerializer.js.map +1 -0
  250. package/dist/serializers/index.d.ts +8 -0
  251. package/dist/serializers/index.d.ts.map +1 -0
  252. package/dist/serializers/index.js +8 -0
  253. package/dist/serializers/index.js.map +1 -0
  254. package/package.json +27 -16
  255. package/src/ConfluenceAuth.ts +571 -0
  256. package/src/ConfluenceClient.ts +188 -156
  257. package/src/ConfluenceConfig.ts +63 -7
  258. package/src/ConfluenceError.ts +110 -14
  259. package/src/GitError.ts +92 -0
  260. package/src/GitService.ts +859 -0
  261. package/src/LocalFileSystem.ts +179 -9
  262. package/src/MarkdownConverter.ts +126 -122
  263. package/src/SchemaConverterError.ts +108 -0
  264. package/src/Schemas.ts +223 -6
  265. package/src/SyncEngine.ts +745 -162
  266. package/src/ast/BlockNode.ts +425 -0
  267. package/src/ast/Document.ts +90 -0
  268. package/src/ast/InlineNode.ts +323 -0
  269. package/src/ast/MacroNode.ts +245 -0
  270. package/src/ast/index.ts +83 -0
  271. package/src/bin.ts +50 -249
  272. package/src/commands/auth.ts +117 -0
  273. package/src/commands/clone.ts +145 -0
  274. package/src/commands/delete.ts +57 -0
  275. package/src/commands/errorHandler.ts +32 -0
  276. package/src/commands/git.ts +114 -0
  277. package/src/commands/index.ts +10 -0
  278. package/src/commands/layers.ts +211 -0
  279. package/src/commands/new.ts +99 -0
  280. package/src/commands/pageTree.ts +40 -0
  281. package/src/commands/shared.ts +35 -0
  282. package/src/commands/sync.ts +129 -0
  283. package/src/index.ts +21 -1
  284. package/src/internal/NodeLayers.ts +21 -0
  285. package/src/internal/frontmatter.ts +21 -0
  286. package/src/internal/gitCommands.ts +229 -0
  287. package/src/internal/hashUtils.ts +65 -3
  288. package/src/internal/oauthServer.ts +199 -0
  289. package/src/internal/pathUtils.ts +34 -17
  290. package/src/internal/tokenStorage.ts +240 -0
  291. package/src/internal/userCache.ts +90 -0
  292. package/src/parsers/ConfluenceParser.ts +950 -0
  293. package/src/parsers/MarkdownParser.ts +1198 -0
  294. package/src/parsers/index.ts +8 -0
  295. package/src/schemas/ConfluenceSchema.ts +56 -0
  296. package/src/schemas/ConversionSchema.ts +318 -0
  297. package/src/schemas/MarkdownSchema.ts +56 -0
  298. package/src/schemas/hast/HastFromHtml.ts +153 -0
  299. package/src/schemas/hast/HastSchema.ts +274 -0
  300. package/src/schemas/hast/index.ts +35 -0
  301. package/src/schemas/index.ts +20 -0
  302. package/src/schemas/mdast/MdastFromMarkdown.ts +118 -0
  303. package/src/schemas/mdast/MdastSchema.ts +566 -0
  304. package/src/schemas/mdast/index.ts +59 -0
  305. package/src/schemas/mdast/mdastToString.ts +102 -0
  306. package/src/schemas/nodes/block/BlockSchema.ts +773 -0
  307. package/src/schemas/nodes/block/index.ts +13 -0
  308. package/src/schemas/nodes/index.ts +20 -0
  309. package/src/schemas/nodes/inline/InlineSchema.ts +523 -0
  310. package/src/schemas/nodes/inline/index.ts +14 -0
  311. package/src/schemas/nodes/macro/MacroSchema.ts +226 -0
  312. package/src/schemas/nodes/macro/index.ts +6 -0
  313. package/src/schemas/preprocessing/ConfluencePreprocessor.ts +446 -0
  314. package/src/schemas/preprocessing/index.ts +8 -0
  315. package/src/serializers/ConfluenceSerializer.ts +717 -0
  316. package/src/serializers/MarkdownSerializer.ts +493 -0
  317. package/src/serializers/index.ts +8 -0
  318. package/test/GitService.test.ts +209 -0
  319. package/test/MarkdownConverter.test.ts +37 -3
  320. package/test/Schemas.test.ts +97 -2
  321. package/test/ast/BlockNode.test.ts +265 -0
  322. package/test/ast/Document.test.ts +126 -0
  323. package/test/ast/InlineNode.test.ts +161 -0
  324. package/test/fixtures/integration-test.html.fixture +103 -0
  325. package/test/fixtures/integration-test.md.expected +257 -0
  326. package/test/integration.test.ts +269 -0
  327. package/test/oauthServer.test.ts +50 -0
  328. package/test/parsers/ConfluenceParser.test.ts +283 -0
  329. package/test/schemas/ConfluencePreprocessor.test.ts +180 -0
  330. package/test/schemas/ConversionSchema.test.ts +159 -0
  331. package/test/schemas/HastSchema.test.ts +138 -0
  332. package/test/schemas/MdastSchema.test.ts +145 -0
  333. package/test/schemas/nodes/block/BlockSchema.test.ts +173 -0
  334. package/test/schemas/nodes/inline/InlineSchema.test.ts +198 -0
  335. package/test/schemas/nodes/macro/MacroSchema.test.ts +142 -0
  336. package/test/tokenStorage.test.ts +99 -0
@@ -0,0 +1,571 @@
1
+ /**
2
+ * OAuth authentication service for Confluence.
3
+ *
4
+ * @module
5
+ */
6
+ import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
7
+ import * as NodePath from "@effect/platform-node/NodePath"
8
+ import * as Command from "@effect/platform/Command"
9
+ import * as CommandExecutor from "@effect/platform/CommandExecutor"
10
+ import * as HttpClient from "@effect/platform/HttpClient"
11
+ import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
12
+ import * as Context from "effect/Context"
13
+ import * as Deferred from "effect/Deferred"
14
+ import * as Effect from "effect/Effect"
15
+ import * as Layer from "effect/Layer"
16
+ import * as Option from "effect/Option"
17
+ import * as Ref from "effect/Ref"
18
+ import * as Schema from "effect/Schema"
19
+ import type { FileSystemError } from "./ConfluenceError.js"
20
+ import { AuthMissingError, OAuthError } from "./ConfluenceError.js"
21
+ import { HttpServerFactoryLive } from "./internal/NodeLayers.js"
22
+ import { startCallbackServer } from "./internal/oauthServer.js"
23
+ import {
24
+ deleteToken,
25
+ HomeDirectoryLive,
26
+ loadOAuthConfig,
27
+ loadToken,
28
+ saveOAuthConfig,
29
+ saveToken
30
+ } from "./internal/tokenStorage.js"
31
+ import type { OAuthConfig, OAuthToken, OAuthUser } from "./Schemas.js"
32
+
33
+ // Layer for token storage operations (FileSystem + Path + HomeDirectory)
34
+ const TokenStorageLive = Layer.mergeAll(
35
+ NodeFileSystem.layer,
36
+ NodePath.layer,
37
+ HomeDirectoryLive
38
+ )
39
+
40
+ /**
41
+ * Generate a cryptographically secure UUID v4.
42
+ * Uses Web Crypto API (available in all modern runtimes).
43
+ * Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
44
+ */
45
+ const generateUUID = (): Effect.Effect<string> =>
46
+ Effect.sync(() => {
47
+ const bytes = new Uint8Array(16)
48
+ globalThis.crypto.getRandomValues(bytes)
49
+
50
+ // Set version (4) and variant bits per RFC 4122
51
+ bytes[6] = (bytes[6]! & 0x0f) | 0x40 // version 4
52
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80 // variant 10xx
53
+
54
+ const hex = Array.from(bytes)
55
+ .map((b) => b.toString(16).padStart(2, "0"))
56
+ .join("")
57
+
58
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`
59
+ })
60
+
61
+ const OAUTH_SCOPES = [
62
+ // Granular scopes (v2 API)
63
+ "read:page:confluence",
64
+ "write:page:confluence",
65
+ "delete:page:confluence",
66
+ // User identity
67
+ "read:me",
68
+ "offline_access"
69
+ ].join(" ")
70
+
71
+ // API endpoints
72
+ const AUTH_URL = "https://auth.atlassian.com/authorize"
73
+ const TOKEN_URL = "https://auth.atlassian.com/oauth/token"
74
+ const REVOKE_URL = "https://auth.atlassian.com/oauth/revoke"
75
+ const RESOURCES_URL = "https://api.atlassian.com/oauth/token/accessible-resources"
76
+ const ME_URL = "https://api.atlassian.com/me"
77
+
78
+ // Response schemas
79
+ const TokenResponseSchema = Schema.Struct({
80
+ access_token: Schema.String,
81
+ refresh_token: Schema.String,
82
+ expires_in: Schema.Number,
83
+ scope: Schema.String,
84
+ token_type: Schema.String
85
+ })
86
+
87
+ const AccessibleResourceSchema = Schema.Struct({
88
+ id: Schema.String,
89
+ name: Schema.String,
90
+ url: Schema.String,
91
+ scopes: Schema.Array(Schema.String)
92
+ })
93
+
94
+ const UserInfoSchema = Schema.Struct({
95
+ account_id: Schema.String,
96
+ name: Schema.String,
97
+ email: Schema.String
98
+ })
99
+
100
+ /**
101
+ * Options for the login method.
102
+ */
103
+ export interface LoginOptions {
104
+ /** Site URL to select (for accounts with multiple sites) */
105
+ readonly siteUrl?: string
106
+ }
107
+
108
+ /**
109
+ * Information about an accessible Confluence site.
110
+ */
111
+ export interface AccessibleSite {
112
+ readonly id: string
113
+ readonly name: string
114
+ readonly url: string
115
+ }
116
+
117
+ /**
118
+ * ConfluenceAuth service interface.
119
+ *
120
+ * @category Services
121
+ */
122
+ export interface ConfluenceAuthService {
123
+ /** Configure OAuth client credentials */
124
+ readonly configure: (config: OAuthConfig) => Effect.Effect<void, FileSystemError>
125
+ /** Check if OAuth is configured */
126
+ readonly isConfigured: () => Effect.Effect<boolean, FileSystemError>
127
+ /** Start OAuth login flow. Returns list of sites if multiple are available. */
128
+ readonly login: (
129
+ options?: LoginOptions
130
+ ) => Effect.Effect<ReadonlyArray<AccessibleSite> | void, OAuthError | FileSystemError>
131
+ /** Remove stored authentication */
132
+ readonly logout: () => Effect.Effect<void, OAuthError | FileSystemError>
133
+ /** Get access token, refreshing if needed */
134
+ readonly getAccessToken: () => Effect.Effect<string, AuthMissingError | OAuthError | FileSystemError>
135
+ /** Get cloud ID from stored token */
136
+ readonly getCloudId: () => Effect.Effect<string, AuthMissingError | FileSystemError>
137
+ /** Get current user info from stored token */
138
+ readonly getCurrentUser: () => Effect.Effect<OAuthUser | null, FileSystemError>
139
+ /** Check if user is logged in */
140
+ readonly isLoggedIn: () => Effect.Effect<boolean, FileSystemError>
141
+ }
142
+
143
+ /**
144
+ * ConfluenceAuth service tag.
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * import { Effect } from "effect"
149
+ * import { ConfluenceAuth } from "@knpkv/confluence-to-markdown/ConfluenceAuth"
150
+ *
151
+ * Effect.gen(function* () {
152
+ * const auth = yield* ConfluenceAuth
153
+ * const isLoggedIn = yield* auth.isLoggedIn()
154
+ * if (!isLoggedIn) {
155
+ * yield* auth.login()
156
+ * }
157
+ * })
158
+ * ```
159
+ *
160
+ * @category Services
161
+ */
162
+ export class ConfluenceAuth extends Context.Tag("@knpkv/confluence-to-markdown/ConfluenceAuth")<
163
+ ConfluenceAuth,
164
+ ConfluenceAuthService
165
+ >() {}
166
+
167
+ const buildAuthUrl = (clientId: string, state: string, port: number): string => {
168
+ const params = new URLSearchParams({
169
+ audience: "api.atlassian.com",
170
+ client_id: clientId,
171
+ scope: OAUTH_SCOPES,
172
+ redirect_uri: `http://localhost:${port}/callback`,
173
+ state,
174
+ response_type: "code",
175
+ prompt: "consent"
176
+ })
177
+ return `${AUTH_URL}?${params.toString()}`
178
+ }
179
+
180
+ // Wrap token storage operations with their required layers
181
+ const loadTokenOp = () => loadToken().pipe(Effect.provide(TokenStorageLive))
182
+ const saveTokenOp = (token: OAuthToken) => saveToken(token).pipe(Effect.provide(TokenStorageLive))
183
+ const deleteTokenOp = () => deleteToken().pipe(Effect.provide(TokenStorageLive))
184
+ const loadOAuthConfigOp = () => loadOAuthConfig().pipe(Effect.provide(TokenStorageLive))
185
+ const saveOAuthConfigOp = (config: OAuthConfig) => saveOAuthConfig(config).pipe(Effect.provide(TokenStorageLive))
186
+
187
+ const make = Effect.gen(function*() {
188
+ const httpClient = yield* HttpClient.HttpClient
189
+ const commandExecutor = yield* CommandExecutor.CommandExecutor
190
+
191
+ // Ref to track ongoing refresh operation to prevent concurrent refreshes
192
+ const refreshLock = yield* Ref.make<Option.Option<Deferred.Deferred<OAuthToken, OAuthError | FileSystemError>>>(
193
+ Option.none()
194
+ )
195
+
196
+ const openBrowserImpl = (url: string): Effect.Effect<void, OAuthError> =>
197
+ Effect.gen(function*() {
198
+ const platform = process.platform
199
+ let command: Command.Command
200
+
201
+ if (platform === "darwin") {
202
+ command = Command.make("open", url)
203
+ } else if (platform === "win32") {
204
+ command = Command.make("cmd", "/c", "start", "", url)
205
+ } else {
206
+ command = Command.make("xdg-open", url)
207
+ }
208
+
209
+ yield* Command.exitCode(command).pipe(
210
+ Effect.provide(Layer.succeed(CommandExecutor.CommandExecutor, commandExecutor))
211
+ )
212
+ }).pipe(
213
+ Effect.mapError((cause) => new OAuthError({ step: "authorize", cause }))
214
+ )
215
+
216
+ const getConfig = (): Effect.Effect<OAuthConfig, OAuthError | FileSystemError> =>
217
+ Effect.gen(function*() {
218
+ const config = yield* loadOAuthConfigOp()
219
+ if (config === null) {
220
+ return yield* Effect.fail(
221
+ new OAuthError({
222
+ step: "authorize",
223
+ cause: "OAuth not configured. Run 'confluence auth configure' first."
224
+ })
225
+ )
226
+ }
227
+ return config
228
+ })
229
+
230
+ const exchangeCodeForTokens = (
231
+ code: string,
232
+ config: OAuthConfig,
233
+ port: number
234
+ ): Effect.Effect<Schema.Schema.Type<typeof TokenResponseSchema>, OAuthError> =>
235
+ Effect.gen(function*() {
236
+ const request = yield* HttpClientRequest.post(TOKEN_URL).pipe(
237
+ HttpClientRequest.setHeader("Content-Type", "application/json"),
238
+ HttpClientRequest.bodyJson({
239
+ grant_type: "authorization_code",
240
+ client_id: config.clientId,
241
+ client_secret: config.clientSecret,
242
+ code,
243
+ redirect_uri: `http://localhost:${port}/callback`
244
+ })
245
+ )
246
+
247
+ const response = yield* httpClient.execute(request)
248
+ const body = yield* response.json
249
+
250
+ return yield* Schema.decodeUnknown(TokenResponseSchema)(body)
251
+ }).pipe(
252
+ Effect.mapError((cause) => new OAuthError({ step: "token", cause }))
253
+ )
254
+
255
+ const getAccessibleResources = (
256
+ accessToken: string
257
+ ): Effect.Effect<ReadonlyArray<Schema.Schema.Type<typeof AccessibleResourceSchema>>, OAuthError> =>
258
+ Effect.gen(function*() {
259
+ const request = HttpClientRequest.get(RESOURCES_URL).pipe(
260
+ HttpClientRequest.setHeader("Authorization", `Bearer ${accessToken}`),
261
+ HttpClientRequest.setHeader("Accept", "application/json")
262
+ )
263
+
264
+ const response = yield* httpClient.execute(request)
265
+ const body = yield* response.json
266
+
267
+ return yield* Schema.decodeUnknown(Schema.Array(AccessibleResourceSchema))(body)
268
+ }).pipe(
269
+ Effect.mapError((cause) => new OAuthError({ step: "authorize", cause }))
270
+ )
271
+
272
+ const getUserInfo = (
273
+ accessToken: string
274
+ ): Effect.Effect<OAuthUser, OAuthError> =>
275
+ Effect.gen(function*() {
276
+ const request = HttpClientRequest.get(ME_URL).pipe(
277
+ HttpClientRequest.setHeader("Authorization", `Bearer ${accessToken}`),
278
+ HttpClientRequest.setHeader("Accept", "application/json")
279
+ )
280
+
281
+ const response = yield* httpClient.execute(request)
282
+ const body = yield* response.json
283
+
284
+ return yield* Schema.decodeUnknown(UserInfoSchema)(body)
285
+ }).pipe(
286
+ Effect.mapError((cause) => new OAuthError({ step: "authorize", cause }))
287
+ )
288
+
289
+ const refreshToken = (
290
+ token: OAuthToken,
291
+ config: OAuthConfig
292
+ ): Effect.Effect<OAuthToken, OAuthError | FileSystemError> =>
293
+ Effect.gen(function*() {
294
+ const request = yield* HttpClientRequest.post(TOKEN_URL).pipe(
295
+ HttpClientRequest.setHeader("Content-Type", "application/json"),
296
+ HttpClientRequest.bodyJson({
297
+ grant_type: "refresh_token",
298
+ client_id: config.clientId,
299
+ client_secret: config.clientSecret,
300
+ refresh_token: token.refresh_token
301
+ }),
302
+ Effect.mapError((cause) => new OAuthError({ step: "refresh", cause }))
303
+ )
304
+
305
+ const response = yield* httpClient.execute(request).pipe(
306
+ Effect.mapError((cause) => new OAuthError({ step: "refresh", cause }))
307
+ )
308
+ const body = yield* response.json.pipe(
309
+ Effect.mapError((cause) => new OAuthError({ step: "refresh", cause }))
310
+ )
311
+ const tokenResponse = yield* Schema.decodeUnknown(TokenResponseSchema)(body).pipe(
312
+ Effect.mapError((cause) => new OAuthError({ step: "refresh", cause }))
313
+ )
314
+
315
+ const updated: OAuthToken = {
316
+ ...token,
317
+ access_token: tokenResponse.access_token,
318
+ refresh_token: tokenResponse.refresh_token,
319
+ expires_at: Date.now() + tokenResponse.expires_in * 1000,
320
+ scope: tokenResponse.scope
321
+ }
322
+
323
+ yield* saveTokenOp(updated)
324
+ return updated
325
+ })
326
+
327
+ const revokeToken = (
328
+ token: OAuthToken,
329
+ config: OAuthConfig
330
+ ): Effect.Effect<void, OAuthError> =>
331
+ Effect.gen(function*() {
332
+ // Revoke refresh token (this also invalidates access token)
333
+ const request = yield* HttpClientRequest.post(REVOKE_URL).pipe(
334
+ HttpClientRequest.setHeader("Content-Type", "application/json"),
335
+ HttpClientRequest.bodyJson({
336
+ client_id: config.clientId,
337
+ client_secret: config.clientSecret,
338
+ token: token.refresh_token
339
+ }),
340
+ Effect.mapError((cause) => new OAuthError({ step: "revoke", cause }))
341
+ )
342
+
343
+ const response = yield* httpClient.execute(request).pipe(
344
+ Effect.mapError((cause) => new OAuthError({ step: "revoke", cause }))
345
+ )
346
+
347
+ // Validate response status
348
+ if (response.status >= 400) {
349
+ return yield* Effect.fail(
350
+ new OAuthError({
351
+ step: "revoke",
352
+ cause: `Token revocation failed with status ${response.status}`
353
+ })
354
+ )
355
+ }
356
+ })
357
+
358
+ const configure: ConfluenceAuthService["configure"] = (config) => saveOAuthConfigOp(config)
359
+
360
+ const isConfigured: ConfluenceAuthService["isConfigured"] = () =>
361
+ Effect.gen(function*() {
362
+ const config = yield* loadOAuthConfigOp()
363
+ return config !== null
364
+ })
365
+
366
+ const login: ConfluenceAuthService["login"] = (options) =>
367
+ Effect.gen(function*() {
368
+ const config = yield* getConfig()
369
+ const state = yield* generateUUID()
370
+
371
+ const { codePromise, port, shutdown } = yield* startCallbackServer(state).pipe(
372
+ Effect.provide(HttpServerFactoryLive)
373
+ )
374
+ const authUrl = buildAuthUrl(config.clientId, state, port)
375
+
376
+ yield* Effect.log(`Opening browser for Atlassian login (callback on port ${port})...`)
377
+ yield* openBrowserImpl(authUrl)
378
+ yield* Effect.log("Waiting for authorization (press Ctrl+C to cancel)...")
379
+
380
+ const code = yield* codePromise.pipe(
381
+ Effect.timeout("5 minutes"),
382
+ Effect.catchTag("TimeoutException", () =>
383
+ Effect.fail(new OAuthError({ step: "authorize", cause: "Authorization timed out" })))
384
+ )
385
+
386
+ yield* shutdown
387
+
388
+ yield* Effect.log("Exchanging code for tokens...")
389
+ const tokens = yield* exchangeCodeForTokens(code, config, port)
390
+
391
+ yield* Effect.log("Fetching accessible sites...")
392
+ const sites = yield* getAccessibleResources(tokens.access_token)
393
+
394
+ if (sites.length === 0) {
395
+ return yield* Effect.fail(
396
+ new OAuthError({
397
+ step: "authorize",
398
+ cause: "No Confluence sites found for this account"
399
+ })
400
+ )
401
+ }
402
+
403
+ let site: Schema.Schema.Type<typeof AccessibleResourceSchema>
404
+
405
+ if (sites.length > 1) {
406
+ // If siteUrl provided, try to match it
407
+ if (options?.siteUrl) {
408
+ const matched = sites.find((s) =>
409
+ s.url === options.siteUrl
410
+ )
411
+ if (!matched) {
412
+ const available = sites.map((s) => ` - ${s.name}: ${s.url}`).join("\n")
413
+ return yield* Effect.fail(
414
+ new OAuthError({
415
+ step: "authorize",
416
+ cause: `Site '${options.siteUrl}' not found. Available sites:\n${available}`
417
+ })
418
+ )
419
+ }
420
+ site = matched
421
+ } else {
422
+ // Return sites list for user to choose
423
+ yield* Effect.log("Multiple Confluence sites found. Please select one:")
424
+ for (const s of sites) {
425
+ yield* Effect.log(` - ${s.name}: ${s.url}`)
426
+ }
427
+ yield* Effect.log("\nRun 'confluence auth login --site <url>' to select a site")
428
+ return sites.map((s) => ({ id: s.id, name: s.name, url: s.url }))
429
+ }
430
+ } else {
431
+ site = sites[0]!
432
+ }
433
+
434
+ yield* Effect.log("Fetching user info...")
435
+ const user = yield* getUserInfo(tokens.access_token)
436
+
437
+ const tokenData: OAuthToken = {
438
+ access_token: tokens.access_token,
439
+ refresh_token: tokens.refresh_token,
440
+ expires_at: Date.now() + tokens.expires_in * 1000,
441
+ scope: tokens.scope,
442
+ cloud_id: site.id,
443
+ site_url: site.url,
444
+ user: {
445
+ account_id: user.account_id,
446
+ name: user.name,
447
+ email: user.email
448
+ }
449
+ }
450
+
451
+ yield* saveTokenOp(tokenData)
452
+ yield* Effect.log(`Logged in as ${user.name} (${user.email})`)
453
+ return undefined
454
+ })
455
+
456
+ const logout: ConfluenceAuthService["logout"] = () =>
457
+ Effect.gen(function*() {
458
+ const token = yield* loadTokenOp()
459
+ if (token === null) {
460
+ yield* Effect.log("Not logged in")
461
+ return
462
+ }
463
+
464
+ // Try to revoke the token with Atlassian (best effort)
465
+ const config = yield* loadOAuthConfigOp()
466
+ if (config !== null) {
467
+ yield* revokeToken(token, config).pipe(
468
+ Effect.tap(() => Effect.log("Token revoked with Atlassian")),
469
+ Effect.catchAll((error) => Effect.log(`Warning: Failed to revoke token: ${error.message}`))
470
+ )
471
+ }
472
+
473
+ yield* deleteTokenOp()
474
+ })
475
+
476
+ const getAccessToken: ConfluenceAuthService["getAccessToken"] = () =>
477
+ Effect.gen(function*() {
478
+ const token = yield* loadTokenOp()
479
+ if (token === null) {
480
+ return yield* Effect.fail(new AuthMissingError())
481
+ }
482
+
483
+ const now = Date.now()
484
+ const buffer = 5 * 60 * 1000 // 5 minutes
485
+
486
+ if (token.expires_at - buffer > now) {
487
+ return token.access_token
488
+ }
489
+
490
+ // Check if refresh is already in progress
491
+ const existing = yield* Ref.get(refreshLock)
492
+ if (Option.isSome(existing)) {
493
+ // Wait for existing refresh to complete
494
+ const refreshed = yield* Deferred.await(existing.value)
495
+ return refreshed.access_token
496
+ }
497
+
498
+ // Start new refresh operation
499
+ const deferred = yield* Deferred.make<OAuthToken, OAuthError | FileSystemError>()
500
+ yield* Ref.set(refreshLock, Option.some(deferred))
501
+
502
+ const config = yield* getConfig()
503
+ yield* Effect.log("Token expired, refreshing...")
504
+
505
+ const result = yield* refreshToken(token, config).pipe(
506
+ Effect.tap((refreshed) => Deferred.succeed(deferred, refreshed)),
507
+ Effect.tapError((error) => Deferred.fail(deferred, error)),
508
+ Effect.ensuring(Ref.set(refreshLock, Option.none())),
509
+ Effect.catchTag("OAuthError", (error) => {
510
+ // If refresh fails (e.g., refresh token expired), clear tokens and prompt re-login
511
+ if (error.step === "refresh") {
512
+ return Effect.gen(function*() {
513
+ yield* deleteTokenOp()
514
+ return yield* Effect.fail(
515
+ new OAuthError({
516
+ step: "refresh",
517
+ cause: "Refresh token expired. Please run 'confluence auth login' to re-authenticate."
518
+ })
519
+ )
520
+ })
521
+ }
522
+ return Effect.fail(error)
523
+ })
524
+ )
525
+
526
+ return result.access_token
527
+ })
528
+
529
+ const getCloudId: ConfluenceAuthService["getCloudId"] = () =>
530
+ Effect.gen(function*() {
531
+ const token = yield* loadTokenOp()
532
+ if (token === null) {
533
+ return yield* Effect.fail(new AuthMissingError())
534
+ }
535
+ return token.cloud_id
536
+ })
537
+
538
+ const getCurrentUser: ConfluenceAuthService["getCurrentUser"] = () =>
539
+ Effect.gen(function*() {
540
+ const token = yield* loadTokenOp()
541
+ return token?.user ?? null
542
+ })
543
+
544
+ const isLoggedIn: ConfluenceAuthService["isLoggedIn"] = () =>
545
+ Effect.gen(function*() {
546
+ const token = yield* loadTokenOp()
547
+ return token !== null
548
+ })
549
+
550
+ return ConfluenceAuth.of({
551
+ configure,
552
+ isConfigured,
553
+ login,
554
+ logout,
555
+ getAccessToken,
556
+ getCloudId,
557
+ getCurrentUser,
558
+ isLoggedIn
559
+ })
560
+ })
561
+
562
+ /**
563
+ * Layer for ConfluenceAuth service.
564
+ *
565
+ * @category Layers
566
+ */
567
+ export const layer: Layer.Layer<
568
+ ConfluenceAuth,
569
+ never,
570
+ HttpClient.HttpClient | CommandExecutor.CommandExecutor
571
+ > = Layer.effect(ConfluenceAuth, make)