@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.
- package/CHANGELOG.md +24 -0
- package/dist/Brand.d.ts.map +1 -1
- package/dist/Brand.js +4 -2
- package/dist/Brand.js.map +1 -1
- package/dist/ast/Document.d.ts +2 -2
- package/dist/ast/Document.d.ts.map +1 -1
- package/dist/ast/Document.js +1 -1
- package/dist/ast/Document.js.map +1 -1
- package/dist/ast/MacroNode.d.ts +4 -4
- package/dist/ast/MacroNode.d.ts.map +1 -1
- package/dist/ast/MacroNode.js +8 -1
- package/dist/ast/MacroNode.js.map +1 -1
- package/dist/attachments.d.ts +42 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +51 -0
- package/dist/attachments.js.map +1 -0
- package/dist/auth/OAuthEndpoints.d.ts +2 -0
- package/dist/auth/OAuthEndpoints.d.ts.map +1 -1
- package/dist/auth/OAuthEndpoints.js +3 -1
- package/dist/auth/OAuthEndpoints.js.map +1 -1
- package/dist/auth/OAuthOperations.d.ts +2 -0
- package/dist/auth/OAuthOperations.d.ts.map +1 -1
- package/dist/auth/OAuthOperations.js +1 -1
- package/dist/auth/OAuthOperations.js.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/config/AuthProfiles.js.map +1 -1
- package/dist/config/ProfileManager.d.ts +3 -20
- package/dist/config/ProfileManager.d.ts.map +1 -1
- package/dist/config/ProfileManager.js +2 -1
- package/dist/config/ProfileManager.js.map +1 -1
- package/dist/config/ProfileStorage.d.ts +5 -0
- package/dist/config/ProfileStorage.d.ts.map +1 -0
- package/dist/config/ProfileStorage.js +5 -0
- package/dist/config/ProfileStorage.js.map +1 -0
- package/dist/config/TokenStorage.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/serializers/MarkdownSerializer.d.ts.map +1 -1
- package/dist/serializers/MarkdownSerializer.js +9 -6
- package/dist/serializers/MarkdownSerializer.js.map +1 -1
- package/package.json +16 -8
- package/src/Brand.ts +4 -2
- package/src/ast/Document.ts +1 -1
- package/src/ast/MacroNode.ts +8 -1
- package/src/attachments.ts +98 -0
- package/src/auth/OAuthEndpoints.ts +5 -1
- package/src/auth/OAuthOperations.ts +3 -1
- package/src/bin.ts +1 -1
- package/src/config/AuthProfiles.ts +1 -1
- package/src/config/ProfileManager.ts +21 -9
- package/src/config/ProfileStorage.ts +5 -0
- package/src/config/TokenStorage.ts +1 -1
- package/src/index.ts +3 -0
- package/src/serializers/MarkdownSerializer.ts +22 -37
- package/test/AuthProfiles.test.ts +1 -1
- package/test/OAuthEndpoints.test.ts +28 -1
- package/test/OAuthOperations.test.ts +16 -0
- package/test/ProfileManager.test.ts +1 -1
- package/test/attachments.test.ts +75 -0
- package/test/fixtures/attachments/inline-attachment.svg +6 -0
- package/tsconfig.build.json +9 -0
- 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:
|
|
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
|
|
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:
|
|
286
|
+
node: BlockNode,
|
|
295
287
|
options: SerializeOptions
|
|
296
288
|
): Effect.Effect<string, SerializeError> =>
|
|
297
289
|
Effect.gen(function*() {
|
|
298
|
-
|
|
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
|
|
294
|
+
return yield* serializeParagraph(node)
|
|
306
295
|
case "CodeBlock":
|
|
307
|
-
return serializeCodeBlock(node
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
return
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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 {
|
|
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*() {
|
|
@@ -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("")
|
|
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("")
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("replaces a named local Markdown placeholder", () => {
|
|
51
|
+
const result = replaceAttachmentPlaceholder(
|
|
52
|
+
"before\n\nafter",
|
|
53
|
+
"./diagram.svg",
|
|
54
|
+
""
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
expect(result).toEqual({
|
|
58
|
+
replacements: 1,
|
|
59
|
+
content: "before\n\nafter"
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("passes the placeholder label to callback replacements", () => {
|
|
64
|
+
const result = replaceAttachmentPlaceholder(
|
|
65
|
+
"before\n\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\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>
|