@knpkv/atlassian-common 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/Brand.d.ts.map +1 -1
  3. package/dist/Brand.js +4 -2
  4. package/dist/Brand.js.map +1 -1
  5. package/dist/ast/Document.d.ts +2 -2
  6. package/dist/ast/Document.d.ts.map +1 -1
  7. package/dist/ast/Document.js +1 -1
  8. package/dist/ast/Document.js.map +1 -1
  9. package/dist/ast/MacroNode.d.ts +4 -4
  10. package/dist/ast/MacroNode.d.ts.map +1 -1
  11. package/dist/ast/MacroNode.js +8 -1
  12. package/dist/ast/MacroNode.js.map +1 -1
  13. package/dist/attachments.d.ts +42 -0
  14. package/dist/attachments.d.ts.map +1 -0
  15. package/dist/attachments.js +51 -0
  16. package/dist/attachments.js.map +1 -0
  17. package/dist/auth/OAuthEndpoints.d.ts +2 -0
  18. package/dist/auth/OAuthEndpoints.d.ts.map +1 -1
  19. package/dist/auth/OAuthEndpoints.js +3 -1
  20. package/dist/auth/OAuthEndpoints.js.map +1 -1
  21. package/dist/auth/OAuthOperations.d.ts +2 -0
  22. package/dist/auth/OAuthOperations.d.ts.map +1 -1
  23. package/dist/auth/OAuthOperations.js +1 -1
  24. package/dist/auth/OAuthOperations.js.map +1 -1
  25. package/dist/bin.js +1 -1
  26. package/dist/bin.js.map +1 -1
  27. package/dist/config/AuthProfiles.js.map +1 -1
  28. package/dist/config/ProfileManager.d.ts +3 -20
  29. package/dist/config/ProfileManager.d.ts.map +1 -1
  30. package/dist/config/ProfileManager.js +2 -1
  31. package/dist/config/ProfileManager.js.map +1 -1
  32. package/dist/config/ProfileStorage.d.ts +5 -0
  33. package/dist/config/ProfileStorage.d.ts.map +1 -0
  34. package/dist/config/ProfileStorage.js +5 -0
  35. package/dist/config/ProfileStorage.js.map +1 -0
  36. package/dist/config/TokenStorage.js.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/serializers/MarkdownSerializer.d.ts.map +1 -1
  42. package/dist/serializers/MarkdownSerializer.js +9 -6
  43. package/dist/serializers/MarkdownSerializer.js.map +1 -1
  44. package/package.json +16 -8
  45. package/src/Brand.ts +4 -2
  46. package/src/ast/Document.ts +1 -1
  47. package/src/ast/MacroNode.ts +8 -1
  48. package/src/attachments.ts +98 -0
  49. package/src/auth/OAuthEndpoints.ts +5 -1
  50. package/src/auth/OAuthOperations.ts +3 -1
  51. package/src/bin.ts +1 -1
  52. package/src/config/AuthProfiles.ts +1 -1
  53. package/src/config/ProfileManager.ts +21 -9
  54. package/src/config/ProfileStorage.ts +5 -0
  55. package/src/config/TokenStorage.ts +1 -1
  56. package/src/index.ts +3 -0
  57. package/src/serializers/MarkdownSerializer.ts +22 -37
  58. package/test/AuthProfiles.test.ts +1 -1
  59. package/test/OAuthEndpoints.test.ts +28 -1
  60. package/test/OAuthOperations.test.ts +16 -0
  61. package/test/ProfileManager.test.ts +1 -1
  62. package/test/attachments.test.ts +75 -0
  63. package/test/fixtures/attachments/inline-attachment.svg +6 -0
  64. package/tsconfig.build.json +9 -0
  65. package/tsconfig.tsbuildinfo +0 -1
@@ -22,8 +22,10 @@
22
22
  * @module
23
23
  */
24
24
  import * as Effect from "effect/Effect"
25
+ import type { BlockNode, BlockQuote, List } from "../ast/BlockNode.js"
25
26
  import type { Document, DocumentNode } from "../ast/Document.js"
26
27
  import type { InlineNode } from "../ast/InlineNode.js"
28
+ import type { ExpandMacro, InfoPanel } from "../ast/MacroNode.js"
27
29
  import type { SerializeError } from "../SerializeError.js"
28
30
 
29
31
  /**
@@ -242,28 +244,18 @@ const serializeTable = (
242
244
  return lines.join("\n")
243
245
  })
244
246
 
245
- // Simple block type for list items - use unknown for flexibility
246
- type SimpleBlock = unknown
247
-
248
- // List item type
249
- type ListItemType = {
250
- readonly _tag: "ListItem"
251
- readonly checked?: boolean | undefined
252
- readonly children: ReadonlyArray<unknown>
253
- }
254
-
255
247
  /**
256
248
  * Serialize list.
257
249
  */
258
250
  const serializeList = (
259
- node: { ordered: boolean; start?: number | undefined; children: ReadonlyArray<unknown> },
251
+ node: List,
260
252
  options: SerializeOptions
261
253
  ): Effect.Effect<string, SerializeError> =>
262
254
  Effect.gen(function*() {
263
255
  const lines: Array<string> = []
264
256
  let counter = node.start ?? 1
265
257
 
266
- for (const item of node.children as ReadonlyArray<ListItemType>) {
258
+ for (const item of node.children) {
267
259
  const prefix = node.ordered ? `${counter}.` : "-"
268
260
  const checkbox = item.checked !== undefined ? (item.checked ? "[x] " : "[ ] ") : ""
269
261
 
@@ -291,38 +283,31 @@ const serializeList = (
291
283
  * Serialize simple block (for nested content).
292
284
  */
293
285
  const serializeSimpleBlock = (
294
- node: SimpleBlock,
286
+ node: BlockNode,
295
287
  options: SerializeOptions
296
288
  ): Effect.Effect<string, SerializeError> =>
297
289
  Effect.gen(function*() {
298
- const tagged = node as { readonly _tag: string }
299
- switch (tagged._tag) {
290
+ switch (node._tag) {
300
291
  case "Heading":
301
- return yield* serializeHeading(
302
- node as unknown as { level: 1 | 2 | 3 | 4 | 5 | 6; children: ReadonlyArray<InlineNode> }
303
- )
292
+ return yield* serializeHeading(node)
304
293
  case "Paragraph":
305
- return yield* serializeParagraph(node as unknown as { children: ReadonlyArray<InlineNode> })
294
+ return yield* serializeParagraph(node)
306
295
  case "CodeBlock":
307
- return serializeCodeBlock(node as unknown as { code: string; language?: string | undefined })
296
+ return serializeCodeBlock(node)
308
297
  case "ThematicBreak":
309
298
  return "---"
310
299
  case "Image":
311
- return serializeImage(
312
- node as unknown as { src: string; alt?: string | undefined; title?: string | undefined },
313
- options
314
- )
300
+ return serializeImage(node, options)
315
301
  case "Table":
316
- return yield* serializeTable(
317
- node as unknown as {
318
- header?: { cells: ReadonlyArray<{ children: ReadonlyArray<InlineNode> }> } | undefined
319
- rows: ReadonlyArray<{ cells: ReadonlyArray<{ children: ReadonlyArray<InlineNode> }> }>
320
- }
321
- )
322
- case "UnsupportedBlock": {
323
- const unsupported = node as unknown as { rawMarkdown?: string; rawHtml?: string; rawAdf?: string }
324
- return unsupported.rawMarkdown ?? unsupported.rawHtml ?? unsupported.rawAdf ?? ""
325
- }
302
+ return yield* serializeTable(node)
303
+ case "List":
304
+ return yield* serializeList(node, options)
305
+ case "BlockQuote":
306
+ return yield* serializeBlockQuote(node, options)
307
+ case "TaskList":
308
+ return yield* serializeTaskList(node.children, options)
309
+ case "UnsupportedBlock":
310
+ return node.rawMarkdown ?? node.rawHtml ?? node.rawAdf ?? ""
326
311
  default:
327
312
  return ""
328
313
  }
@@ -332,7 +317,7 @@ const serializeSimpleBlock = (
332
317
  * Serialize block quote.
333
318
  */
334
319
  const serializeBlockQuote = (
335
- node: { children: ReadonlyArray<unknown> },
320
+ node: BlockQuote,
336
321
  options: SerializeOptions
337
322
  ): Effect.Effect<string, SerializeError> =>
338
323
  Effect.gen(function*() {
@@ -349,7 +334,7 @@ const serializeBlockQuote = (
349
334
  * Serialize info panel to container syntax.
350
335
  */
351
336
  const serializeInfoPanel = (
352
- node: { panelType: string; title?: string | undefined; children: ReadonlyArray<unknown> },
337
+ node: InfoPanel,
353
338
  options: SerializeOptions
354
339
  ): Effect.Effect<string, SerializeError> =>
355
340
  Effect.gen(function*() {
@@ -370,7 +355,7 @@ const serializeInfoPanel = (
370
355
  * Serialize expand macro.
371
356
  */
372
357
  const serializeExpandMacro = (
373
- node: { title?: string | undefined; children: ReadonlyArray<unknown> },
358
+ node: ExpandMacro,
374
359
  options: SerializeOptions
375
360
  ): Effect.Effect<string, SerializeError> =>
376
361
  Effect.gen(function*() {
@@ -57,7 +57,7 @@ const makeMockFS = (files: Record<string, string> = {}) => {
57
57
 
58
58
  return {
59
59
  store,
60
- layer: Layer.succeed(FileSystem.FileSystem, FileSystem.FileSystem.of(partial as FileSystem.FileSystem))
60
+ layer: Layer.succeed(FileSystem.FileSystem, FileSystem.FileSystem.of(partial))
61
61
  }
62
62
  }
63
63
 
@@ -2,11 +2,24 @@ import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
2
2
  import { it as effectIt } from "@effect/vitest"
3
3
  import * as Effect from "effect/Effect"
4
4
  import { describe, expect, it } from "vitest"
5
- import { AUTH_URL, buildAuthUrl, computeCodeChallenge, generateCodeVerifier } from "../src/auth/OAuthEndpoints.js"
5
+ import {
6
+ AUTH_URL,
7
+ buildAuthUrl,
8
+ computeCodeChallenge,
9
+ CONFLUENCE_SCOPES,
10
+ generateCodeVerifier
11
+ } from "../src/auth/OAuthEndpoints.js"
6
12
 
7
13
  const withCrypto = <A, E, R>(effect: Effect.Effect<A, E, R>) => effect.pipe(Effect.provide(NodeCrypto.layer))
8
14
 
9
15
  describe("OAuthEndpoints", () => {
16
+ describe("CONFLUENCE_SCOPES", () => {
17
+ it("includes attachment read and write scopes", () => {
18
+ expect(CONFLUENCE_SCOPES).toContain("read:attachment:confluence")
19
+ expect(CONFLUENCE_SCOPES).toContain("write:attachment:confluence")
20
+ })
21
+ })
22
+
10
23
  // ---- PKCE ----
11
24
 
12
25
  describe("generateCodeVerifier", () => {
@@ -77,6 +90,20 @@ describe("OAuthEndpoints", () => {
77
90
  expect(parsed.searchParams.get("prompt")).toBe("consent")
78
91
  })
79
92
 
93
+ it("uses an application callback URL when supplied", () => {
94
+ const url = buildAuthUrl({
95
+ clientId: "cid",
96
+ state: "state-123",
97
+ port: 4173,
98
+ redirectUri: "http://127.0.0.1:4173/services/oauth/atlassian/callback",
99
+ scopes: ["read:me"]
100
+ })
101
+
102
+ expect(new URL(url).searchParams.get("redirect_uri")).toBe(
103
+ "http://127.0.0.1:4173/services/oauth/atlassian/callback"
104
+ )
105
+ })
106
+
80
107
  // PKCE is optional — omitting codeChallenge must not add params (backwards compat)
81
108
  it("omits PKCE params when codeChallenge not provided", () => {
82
109
  const url = buildAuthUrl({
@@ -127,6 +127,22 @@ describe("OAuthOperations", () => {
127
127
  expect(body.code_verifier).toBe("my-verifier")
128
128
  }))
129
129
 
130
+ it.effect("uses the same application callback URL during exchange", () =>
131
+ Effect.gen(function*() {
132
+ const { capturedRequests, mockClient } = createMockHttpClient([{ status: 200, body: validTokenBody }])
133
+ const redirectUri = "http://127.0.0.1:4173/services/oauth/atlassian/callback"
134
+
135
+ yield* exchangeCodeForTokens("auth-code", oauthConfig, { port: 4173, redirectUri }).pipe(
136
+ provide(mockClient)
137
+ )
138
+
139
+ const request = capturedRequests[0]!
140
+ const body = yield* request.body._tag === "Uint8Array"
141
+ ? Effect.succeed(JSON.parse(new TextDecoder().decode(request.body.body)))
142
+ : Effect.succeed({})
143
+ expect(body.redirect_uri).toBe(redirectUri)
144
+ }))
145
+
130
146
  // Token exchange failures (invalid code, expired code) must surface as OAuthError with step="token"
131
147
  it.effect("returns OAuthError with step=token on HTTP 400", () =>
132
148
  Effect.gen(function*() {
@@ -64,7 +64,7 @@ const makeMockFS = () => {
64
64
  }
65
65
  return {
66
66
  store,
67
- layer: Layer.succeed(FileSystem.FileSystem, FileSystem.FileSystem.of(partial as FileSystem.FileSystem))
67
+ layer: Layer.succeed(FileSystem.FileSystem, FileSystem.FileSystem.of(partial))
68
68
  }
69
69
  }
70
70
 
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { isPreviewableAttachment, renderAttachmentMarkdown, replaceAttachmentPlaceholder } from "../src/attachments.js"
3
+
4
+ describe("attachment helpers", () => {
5
+ it("treats SVG as previewable even with a generic media type", () => {
6
+ expect(isPreviewableAttachment({
7
+ filename: "diagram.svg",
8
+ mediaType: "application/octet-stream"
9
+ })).toBe(true)
10
+ })
11
+
12
+ it("renders image attachments as previews and other attachments as links", () => {
13
+ expect(renderAttachmentMarkdown({
14
+ id: "1",
15
+ filename: "screenshot.png",
16
+ url: "https://example.test/screenshot.png",
17
+ mediaType: "image/png",
18
+ size: 1
19
+ })).toBe("![screenshot.png](https://example.test/screenshot.png)")
20
+
21
+ expect(renderAttachmentMarkdown({
22
+ id: "2",
23
+ filename: "debug.log",
24
+ url: "https://example.test/debug.log",
25
+ mediaType: "text/plain",
26
+ size: 1
27
+ })).toBe("[debug.log](https://example.test/debug.log)")
28
+ })
29
+
30
+ it("escapes labels and wraps unsafe destinations in rendered markdown", () => {
31
+ expect(renderAttachmentMarkdown({
32
+ id: "1",
33
+ filename: "weird ] name\n.svg",
34
+ url: "https://example.test/a file(1).svg",
35
+ mediaType: "image/svg+xml",
36
+ size: 1
37
+ })).toBe("![weird \\] name .svg](<https://example.test/a file(1).svg>)")
38
+ })
39
+
40
+ it("uses an explicit label when rendering attachment markdown", () => {
41
+ expect(renderAttachmentMarkdown({
42
+ id: "1",
43
+ filename: "diagram.svg",
44
+ url: "https://example.test/diagram.svg",
45
+ mediaType: "image/svg+xml",
46
+ size: 1
47
+ }, { label: "Architecture diagram" })).toBe("![Architecture diagram](https://example.test/diagram.svg)")
48
+ })
49
+
50
+ it("replaces a named local Markdown placeholder", () => {
51
+ const result = replaceAttachmentPlaceholder(
52
+ "before\n![Diagram](./diagram.svg)\nafter",
53
+ "./diagram.svg",
54
+ "![diagram.svg](https://example.test/diagram.svg)"
55
+ )
56
+
57
+ expect(result).toEqual({
58
+ replacements: 1,
59
+ content: "before\n![diagram.svg](https://example.test/diagram.svg)\nafter"
60
+ })
61
+ })
62
+
63
+ it("passes the placeholder label to callback replacements", () => {
64
+ const result = replaceAttachmentPlaceholder(
65
+ "before\n![Architecture diagram](./diagram.svg)\nafter",
66
+ "./diagram.svg",
67
+ ({ isImage, label }) => `${isImage ? "!" : ""}[${label}](https://example.test/diagram.svg)`
68
+ )
69
+
70
+ expect(result).toEqual({
71
+ replacements: 1,
72
+ content: "before\n![Architecture diagram](https://example.test/diagram.svg)\nafter"
73
+ })
74
+ })
75
+ })
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="160" height="80" viewBox="0 0 160 80" role="img" aria-label="Inline attachment test asset">
2
+ <rect width="160" height="80" fill="#f4f7fb"/>
3
+ <rect x="12" y="12" width="136" height="56" rx="6" fill="#ffffff" stroke="#2f6fed" stroke-width="3"/>
4
+ <circle cx="40" cy="40" r="13" fill="#ffb000"/>
5
+ <path d="M66 51h58M66 39h72M66 28h44" stroke="#243b53" stroke-width="6" stroke-linecap="round"/>
6
+ </svg>
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "http://json.schemastore.org/tsconfig",
3
+ "extends": "./tsconfig.json",
4
+ "compilerOptions": {
5
+ "composite": true,
6
+ "incremental": true,
7
+ "tsBuildInfoFile": "node_modules/.cache/tsconfig.build.tsbuildinfo"
8
+ }
9
+ }