@knpkv/atlassian-common 0.2.0 → 0.4.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 (80) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +25 -10
  3. package/dist/Brand.d.ts +15 -15
  4. package/dist/Brand.d.ts.map +1 -1
  5. package/dist/Brand.js +5 -5
  6. package/dist/Brand.js.map +1 -1
  7. package/dist/Hash.d.ts +12 -14
  8. package/dist/Hash.d.ts.map +1 -1
  9. package/dist/Hash.js +18 -24
  10. package/dist/Hash.js.map +1 -1
  11. package/dist/ast/BlockNode.d.ts +120 -177
  12. package/dist/ast/BlockNode.d.ts.map +1 -1
  13. package/dist/ast/BlockNode.js +28 -8
  14. package/dist/ast/BlockNode.js.map +1 -1
  15. package/dist/ast/Document.d.ts +114 -156
  16. package/dist/ast/Document.d.ts.map +1 -1
  17. package/dist/ast/Document.js +1 -1
  18. package/dist/ast/Document.js.map +1 -1
  19. package/dist/ast/InlineNode.d.ts +59 -247
  20. package/dist/ast/InlineNode.d.ts.map +1 -1
  21. package/dist/ast/InlineNode.js +28 -3
  22. package/dist/ast/InlineNode.js.map +1 -1
  23. package/dist/ast/MacroNode.d.ts +62 -102
  24. package/dist/ast/MacroNode.d.ts.map +1 -1
  25. package/dist/ast/MacroNode.js +12 -5
  26. package/dist/ast/MacroNode.js.map +1 -1
  27. package/dist/auth/OAuthOperations.d.ts +1 -1
  28. package/dist/auth/OAuthOperations.d.ts.map +1 -1
  29. package/dist/auth/OAuthOperations.js +5 -6
  30. package/dist/auth/OAuthOperations.js.map +1 -1
  31. package/dist/auth/OAuthResponseSchemas.d.ts +13 -13
  32. package/dist/bin.d.ts +3 -0
  33. package/dist/bin.d.ts.map +1 -0
  34. package/dist/bin.js +58 -0
  35. package/dist/bin.js.map +1 -0
  36. package/dist/config/AuthProfiles.d.ts +342 -0
  37. package/dist/config/AuthProfiles.d.ts.map +1 -0
  38. package/dist/config/AuthProfiles.js +275 -0
  39. package/dist/config/AuthProfiles.js.map +1 -0
  40. package/dist/config/ConfigPaths.d.ts +12 -26
  41. package/dist/config/ConfigPaths.d.ts.map +1 -1
  42. package/dist/config/ConfigPaths.js +33 -3
  43. package/dist/config/ConfigPaths.js.map +1 -1
  44. package/dist/config/OAuthSchemas.d.ts +24 -24
  45. package/dist/config/ProfileManager.d.ts +92 -0
  46. package/dist/config/ProfileManager.d.ts.map +1 -0
  47. package/dist/config/ProfileManager.js +175 -0
  48. package/dist/config/ProfileManager.js.map +1 -0
  49. package/dist/config/TokenStorage.d.ts +5 -24
  50. package/dist/config/TokenStorage.d.ts.map +1 -1
  51. package/dist/config/TokenStorage.js +35 -22
  52. package/dist/config/TokenStorage.js.map +1 -1
  53. package/dist/config/index.d.ts +3 -1
  54. package/dist/config/index.d.ts.map +1 -1
  55. package/dist/config/index.js +5 -1
  56. package/dist/config/index.js.map +1 -1
  57. package/dist/index.d.ts +1 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +1 -1
  60. package/dist/index.js.map +1 -1
  61. package/package.json +17 -11
  62. package/src/Brand.ts +5 -17
  63. package/src/Hash.ts +27 -27
  64. package/src/ast/BlockNode.ts +13 -13
  65. package/src/ast/Document.ts +1 -1
  66. package/src/ast/InlineNode.ts +5 -5
  67. package/src/ast/MacroNode.ts +7 -6
  68. package/src/auth/OAuthOperations.ts +5 -6
  69. package/src/bin.ts +115 -0
  70. package/src/config/AuthProfiles.ts +384 -0
  71. package/src/config/ConfigPaths.ts +21 -7
  72. package/src/config/ProfileManager.ts +268 -0
  73. package/src/config/TokenStorage.ts +23 -27
  74. package/src/config/index.ts +42 -0
  75. package/src/index.ts +1 -1
  76. package/test/AuthProfiles.test.ts +131 -0
  77. package/test/Hash.test.ts +52 -0
  78. package/test/OAuthOperations.test.ts +2 -3
  79. package/test/ProfileManager.test.ts +188 -0
  80. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,188 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import { ConfigProvider, Effect, Layer } from "effect"
3
+ import * as FileSystem from "effect/FileSystem"
4
+ import * as Path from "effect/Path"
5
+ import { SystemError } from "effect/PlatformError"
6
+ import {
7
+ type AtlassianToolDefinition,
8
+ HomeDirectoryLive,
9
+ inspectAllToolProfiles,
10
+ migrateLegacyProfiles,
11
+ MissingOAuthConfigError,
12
+ missingScopes,
13
+ type OAuthToken,
14
+ ProfileNotFoundError,
15
+ refreshActiveProfiles,
16
+ saveProfileToken,
17
+ useProfileForAllTools
18
+ } from "../src/config/index.js"
19
+
20
+ const TEST_HOME = "/tmp/atlassian-common-manager-test"
21
+ const tools: ReadonlyArray<AtlassianToolDefinition> = [
22
+ { toolName: "tool-a", label: "Tool A", loginHint: "tool-a login", requiredScopes: ["read:me", "offline_access"] },
23
+ { toolName: "tool-b", label: "Tool B", loginHint: "tool-b login", requiredScopes: ["read:me", "write:jira-work"] }
24
+ ]
25
+ const sharedStoreTools: ReadonlyArray<AtlassianToolDefinition> = [
26
+ tools[0]!,
27
+ {
28
+ toolName: "tool-c",
29
+ authStoreName: "tool-a",
30
+ label: "Tool C",
31
+ loginHint: "tool-c login",
32
+ requiredScopes: ["read:me"]
33
+ }
34
+ ]
35
+
36
+ const fsError = (method: string) =>
37
+ Effect.fail(new SystemError({ _tag: "NotFound", module: "FileSystem", method, description: "mock not found" }))
38
+
39
+ const makeToken = (n: number, scope = "read:me offline_access", expiresAt = Date.now() + 60_000): OAuthToken => ({
40
+ access_token: `access-${n}`,
41
+ refresh_token: `refresh-${n}`,
42
+ expires_at: expiresAt,
43
+ scope,
44
+ cloud_id: `cloud-${n}`,
45
+ site_url: `https://site-${n}.atlassian.net`,
46
+ user: { account_id: `account-${n}`, name: `User ${n}`, email: `user-${n}@example.com` }
47
+ })
48
+
49
+ const makeMockFS = () => {
50
+ const store: Record<string, string> = {}
51
+ const partial: Partial<FileSystem.FileSystem> = {
52
+ exists: (path) => Effect.succeed(path in store),
53
+ readFileString: (path) => path in store ? Effect.succeed(store[path]!) : fsError("readFileString"),
54
+ writeFileString: (path: string, content: string) => {
55
+ store[path] = content
56
+ return Effect.void
57
+ },
58
+ makeDirectory: () => Effect.void,
59
+ chmod: () => Effect.void,
60
+ remove: (path: string) => {
61
+ delete store[path]
62
+ return Effect.void
63
+ }
64
+ }
65
+ return {
66
+ store,
67
+ layer: Layer.succeed(FileSystem.FileSystem, FileSystem.FileSystem.of(partial as FileSystem.FileSystem))
68
+ }
69
+ }
70
+
71
+ const ConfigProviderLive = ConfigProvider.layer(ConfigProvider.fromEnv({ env: { HOME: TEST_HOME } }))
72
+
73
+ const run = async <A>(effect: Effect.Effect<A, unknown, FileSystem.FileSystem | Path.Path>) => {
74
+ const mock = makeMockFS()
75
+ const layer = Layer.mergeAll(mock.layer, Path.layer, HomeDirectoryLive, ConfigProviderLive)
76
+ const result = await Effect.runPromise(effect.pipe(Effect.provide(layer)))
77
+ return { result, store: mock.store }
78
+ }
79
+
80
+ describe("ProfileManager", () => {
81
+ it("reports missing scopes from a token", () => {
82
+ expect(missingScopes(makeToken(1), ["read:me", "write:jira-work"])).toEqual(["write:jira-work"])
83
+ })
84
+
85
+ it("inspects active profile usage and scope status per tool", async () => {
86
+ const { result } = await run(
87
+ Effect.gen(function*() {
88
+ yield* saveProfileToken("tool-a", makeToken(1))
89
+ yield* saveProfileToken("tool-b", makeToken(2))
90
+ return yield* inspectAllToolProfiles(tools)
91
+ })
92
+ )
93
+
94
+ expect(result.map((status) => status.activeProfile?.id)).toEqual(["account-1@cloud-1", "account-2@cloud-2"])
95
+ expect(result[0]!.missingScopes).toEqual([])
96
+ expect(result[1]!.missingScopes).toEqual(["write:jira-work"])
97
+ })
98
+
99
+ it("switches matching profiles without copying tokens between auth stores", async () => {
100
+ const { result, store } = await run(
101
+ Effect.gen(function*() {
102
+ yield* saveProfileToken("tool-a", makeToken(1))
103
+ yield* saveProfileToken("tool-b", makeToken(2, "read:me write:jira-work"))
104
+ return yield* useProfileForAllTools("https://site-2.atlassian.net", tools)
105
+ })
106
+ )
107
+
108
+ expect(result.map((status) => status.activeProfile?.id)).toEqual(["account-1@cloud-1", "account-2@cloud-2"])
109
+ expect(JSON.parse(store[`${TEST_HOME}/.config/atlassian/tool-a/auth.json`]!)["access_token"]).toBe("access-1")
110
+ })
111
+
112
+ it("reports multiple tools that share one auth store", async () => {
113
+ const { result } = await run(
114
+ Effect.gen(function*() {
115
+ yield* saveProfileToken("tool-a", makeToken(1))
116
+ return yield* inspectAllToolProfiles(sharedStoreTools)
117
+ })
118
+ )
119
+
120
+ expect(result.map((status) => status.authStoreName)).toEqual(["tool-a", "tool-a"])
121
+ expect(result.map((status) => status.activeProfile?.id)).toEqual(["account-1@cloud-1", "account-1@cloud-1"])
122
+ })
123
+
124
+ it("migrates XDG legacy auth.json tokens into profile stores", async () => {
125
+ const token = makeToken(3)
126
+ const { result, store } = await run(
127
+ Effect.gen(function*() {
128
+ yield* FileSystem.FileSystem.pipe(
129
+ Effect.flatMap((fs) =>
130
+ fs.writeFileString(`${TEST_HOME}/.config/atlassian/tool-a/auth.json`, JSON.stringify(token, null, 2))
131
+ )
132
+ )
133
+ return yield* migrateLegacyProfiles(tools)
134
+ })
135
+ )
136
+
137
+ expect(result[0]!.activeProfile?.id).toBe("account-3@cloud-3")
138
+ expect(JSON.parse(store[`${TEST_HOME}/.config/atlassian/tool-a/profiles.json`]!)["profiles"]).toHaveLength(1)
139
+ })
140
+
141
+ it("migrates tool-specific legacy auth.json tokens into profile stores", async () => {
142
+ const token = makeToken(4)
143
+ const legacyTools: ReadonlyArray<AtlassianToolDefinition> = [
144
+ {
145
+ toolName: "legacy-tool",
146
+ legacyAuthPath: [".legacy-tool", "auth.json"],
147
+ label: "Legacy Tool",
148
+ loginHint: "legacy login",
149
+ requiredScopes: ["read:me"]
150
+ }
151
+ ]
152
+ const { result, store } = await run(
153
+ Effect.gen(function*() {
154
+ yield* FileSystem.FileSystem.pipe(
155
+ Effect.flatMap((fs) =>
156
+ fs.writeFileString(`${TEST_HOME}/.legacy-tool/auth.json`, JSON.stringify(token, null, 2))
157
+ )
158
+ )
159
+ return yield* migrateLegacyProfiles(legacyTools)
160
+ })
161
+ )
162
+
163
+ expect(result[0]!.activeProfile?.id).toBe("account-4@cloud-4")
164
+ expect(JSON.parse(store[`${TEST_HOME}/.config/atlassian/legacy-tool/profiles.json`]!)["profiles"]).toHaveLength(1)
165
+ })
166
+
167
+ it("fails when selecting a missing profile", async () => {
168
+ await expect(
169
+ run(
170
+ Effect.gen(function*() {
171
+ yield* saveProfileToken("tool-a", makeToken(1))
172
+ return yield* useProfileForAllTools("missing", tools)
173
+ })
174
+ )
175
+ ).rejects.toBeInstanceOf(ProfileNotFoundError)
176
+ })
177
+
178
+ it("fails expired-token refresh when OAuth config is missing", async () => {
179
+ await expect(
180
+ run(
181
+ Effect.gen(function*() {
182
+ yield* saveProfileToken("tool-a", makeToken(1, "read:me offline_access", Date.now() - 1_000))
183
+ return yield* refreshActiveProfiles([tools[0]!])
184
+ })
185
+ )
186
+ ).rejects.toBeInstanceOf(MissingOAuthConfigError)
187
+ })
188
+ })