@knpkv/atlassian-common 1.3.0 → 1.5.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 (72) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +1 -1
  3. package/dist/Brand.d.ts +2 -2
  4. package/dist/Brand.d.ts.map +1 -1
  5. package/dist/Brand.js.map +1 -1
  6. package/dist/Hash.d.ts.map +1 -1
  7. package/dist/Hash.js +2 -2
  8. package/dist/Hash.js.map +1 -1
  9. package/dist/ast/Document.d.ts +3 -19
  10. package/dist/ast/Document.d.ts.map +1 -1
  11. package/dist/ast/Document.js +4 -3
  12. package/dist/ast/Document.js.map +1 -1
  13. package/dist/ast/MacroNode.d.ts +3 -3
  14. package/dist/attachments.d.ts.map +1 -1
  15. package/dist/attachments.js +2 -6
  16. package/dist/attachments.js.map +1 -1
  17. package/dist/auth/OAuthEndpoints.d.ts +13 -0
  18. package/dist/auth/OAuthEndpoints.d.ts.map +1 -1
  19. package/dist/auth/OAuthEndpoints.js +24 -3
  20. package/dist/auth/OAuthEndpoints.js.map +1 -1
  21. package/dist/auth/OAuthErrors.d.ts +22 -0
  22. package/dist/auth/OAuthErrors.d.ts.map +1 -1
  23. package/dist/auth/OAuthErrors.js +1 -1
  24. package/dist/auth/OAuthErrors.js.map +1 -1
  25. package/dist/auth/OAuthOperations.d.ts +8 -2
  26. package/dist/auth/OAuthOperations.d.ts.map +1 -1
  27. package/dist/auth/OAuthOperations.js +18 -8
  28. package/dist/auth/OAuthOperations.js.map +1 -1
  29. package/dist/auth/OAuthResponseSchemas.d.ts +11 -0
  30. package/dist/auth/OAuthResponseSchemas.d.ts.map +1 -1
  31. package/dist/auth/OAuthResponseSchemas.js +11 -0
  32. package/dist/auth/OAuthResponseSchemas.js.map +1 -1
  33. package/dist/auth/index.d.ts +1 -1
  34. package/dist/auth/index.d.ts.map +1 -1
  35. package/dist/auth/index.js +1 -1
  36. package/dist/auth/index.js.map +1 -1
  37. package/dist/bin.js +2 -2
  38. package/dist/bin.js.map +1 -1
  39. package/dist/config/AuthProfiles.d.ts +140 -175
  40. package/dist/config/AuthProfiles.d.ts.map +1 -1
  41. package/dist/config/AuthProfiles.js +2 -1
  42. package/dist/config/AuthProfiles.js.map +1 -1
  43. package/dist/config/ConfigPaths.d.ts.map +1 -1
  44. package/dist/config/ConfigPaths.js +2 -2
  45. package/dist/config/ProfileManager.d.ts +14 -3
  46. package/dist/config/ProfileManager.d.ts.map +1 -1
  47. package/dist/config/ProfileManager.js +33 -5
  48. package/dist/config/ProfileManager.js.map +1 -1
  49. package/dist/config/TokenStorage.d.ts.map +1 -1
  50. package/dist/config/TokenStorage.js +2 -1
  51. package/dist/config/TokenStorage.js.map +1 -1
  52. package/dist/serializers/MarkdownSerializer.d.ts.map +1 -1
  53. package/dist/serializers/MarkdownSerializer.js.map +1 -1
  54. package/package.json +8 -8
  55. package/src/Brand.ts +5 -5
  56. package/src/Hash.ts +2 -2
  57. package/src/ast/Document.ts +5 -4
  58. package/src/attachments.ts +2 -1
  59. package/src/auth/OAuthEndpoints.ts +25 -3
  60. package/src/auth/OAuthErrors.ts +23 -1
  61. package/src/auth/OAuthOperations.ts +33 -20
  62. package/src/auth/OAuthResponseSchemas.ts +12 -0
  63. package/src/auth/index.ts +1 -0
  64. package/src/bin.ts +5 -3
  65. package/src/config/AuthProfiles.ts +4 -2
  66. package/src/config/ConfigPaths.ts +3 -3
  67. package/src/config/ProfileManager.ts +42 -6
  68. package/src/config/TokenStorage.ts +4 -2
  69. package/test/ApiUpdateWorkflows.test.ts +356 -0
  70. package/test/OAuthEndpoints.test.ts +23 -0
  71. package/test/OAuthOperations.test.ts +2 -2
  72. package/test/ProfileManager.test.ts +61 -0
@@ -6,6 +6,7 @@ import {
6
6
  AUTH_URL,
7
7
  buildAuthUrl,
8
8
  computeCodeChallenge,
9
+ CONFLUENCE_FOLDER_SCOPES,
9
10
  CONFLUENCE_SCOPES,
10
11
  generateCodeVerifier
11
12
  } from "../src/auth/OAuthEndpoints.js"
@@ -18,6 +19,28 @@ describe("OAuthEndpoints", () => {
18
19
  expect(CONFLUENCE_SCOPES).toContain("read:attachment:confluence")
19
20
  expect(CONFLUENCE_SCOPES).toContain("write:attachment:confluence")
20
21
  })
22
+
23
+ // Control Center feeds this constant straight into its own authorize
24
+ // request, so folder authority must not leak in here — it would widen every
25
+ // Control Center sign-in to a scope its documented OAuth app never enables.
26
+ it("carries no folder or search authority", () => {
27
+ for (const scope of CONFLUENCE_FOLDER_SCOPES) {
28
+ expect(CONFLUENCE_SCOPES).not.toContain(scope)
29
+ }
30
+ })
31
+ })
32
+
33
+ describe("CONFLUENCE_FOLDER_SCOPES", () => {
34
+ // Exact strings: Atlassian rejects the whole authorize request for one
35
+ // unrecognized scope, and a typo surfaces only on a real consent screen.
36
+ it("names the scopes the folder and CQL-search endpoints require", () => {
37
+ expect([...CONFLUENCE_FOLDER_SCOPES].sort()).toEqual([
38
+ "read:content-details:confluence",
39
+ "read:folder:confluence",
40
+ "read:hierarchical-content:confluence",
41
+ "write:folder:confluence"
42
+ ])
43
+ })
21
44
  })
22
45
 
23
46
  // ---- PKCE ----
@@ -1,9 +1,9 @@
1
1
  import { describe, it } from "@effect/vitest"
2
2
  import * as Effect from "effect/Effect"
3
+ import type { HttpClientRequest } from "effect/http"
4
+ import { HttpClient, HttpClientResponse } from "effect/http"
3
5
  import * as Layer from "effect/Layer"
4
6
  import { TestClock } from "effect/testing"
5
- import type { HttpClientRequest } from "effect/unstable/http"
6
- import { HttpClient, HttpClientResponse } from "effect/unstable/http"
7
7
  import { expect } from "vitest"
8
8
  import { OAuthError } from "../src/auth/OAuthErrors.js"
9
9
  import {
@@ -1,6 +1,10 @@
1
1
  import { describe, expect, it } from "@effect/vitest"
2
2
  import { ConfigProvider, Effect, Layer } from "effect"
3
+ import * as Clock from "effect/Clock"
4
+ import * as Deferred from "effect/Deferred"
5
+ import * as Fiber from "effect/Fiber"
3
6
  import * as FileSystem from "effect/FileSystem"
7
+ import { HttpClient, HttpClientResponse } from "effect/http"
4
8
  import * as Path from "effect/Path"
5
9
  import { SystemError } from "effect/PlatformError"
6
10
  import {
@@ -13,6 +17,7 @@ import {
13
17
  type OAuthToken,
14
18
  ProfileNotFoundError,
15
19
  refreshActiveProfiles,
20
+ saveOAuthConfig,
16
21
  saveProfileToken,
17
22
  useProfileForAllTools
18
23
  } from "../src/config/index.js"
@@ -175,6 +180,62 @@ describe("ProfileManager", () => {
175
180
  ).rejects.toBeInstanceOf(ProfileNotFoundError)
176
181
  })
177
182
 
183
+ // Atlassian rotates refresh tokens, so the grant consumes the stored one and
184
+ // the response carries its replacement. If an interrupt can land between the
185
+ // two, the credential is spent with nothing saved and the profile is silently
186
+ // logged out — hence the uninterruptible region around grant-and-persist.
187
+ it("persists the rotated token even when interrupted mid-rotation", async () => {
188
+ const mock = makeMockFS()
189
+ const program = Effect.gen(function*() {
190
+ yield* saveOAuthConfig("tool-a", { clientId: "client-1", clientSecret: "secret-1" })
191
+ const nowMs = yield* Clock.currentTimeMillis
192
+ yield* saveProfileToken("tool-a", makeToken(1, "read:me offline_access", nowMs - 1_000))
193
+
194
+ const issued = yield* Deferred.make<void>()
195
+ const release = yield* Deferred.make<void>()
196
+ const client = Layer.succeed(
197
+ HttpClient.HttpClient,
198
+ HttpClient.make((request) =>
199
+ Effect.gen(function*() {
200
+ yield* Deferred.succeed(issued, undefined)
201
+ yield* Deferred.await(release)
202
+ return HttpClientResponse.fromWeb(
203
+ request,
204
+ new Response(
205
+ JSON.stringify({
206
+ access_token: "fresh-access",
207
+ refresh_token: "rotated-refresh",
208
+ expires_in: 3600,
209
+ scope: "read:me offline_access",
210
+ token_type: "Bearer"
211
+ }),
212
+ { status: 200, headers: { "content-type": "application/json" } }
213
+ )
214
+ )
215
+ })
216
+ )
217
+ )
218
+
219
+ const fiber = yield* refreshActiveProfiles([tools[0]!]).pipe(
220
+ Effect.provide(client),
221
+ Effect.exit,
222
+ Effect.forkChild({ startImmediately: true })
223
+ )
224
+ yield* Deferred.await(issued)
225
+ const interrupting = yield* Fiber.interrupt(fiber).pipe(Effect.forkChild({ startImmediately: true }))
226
+ yield* Deferred.succeed(release, undefined)
227
+ yield* Fiber.join(interrupting)
228
+ })
229
+
230
+ await Effect.runPromise(
231
+ program.pipe(
232
+ Effect.provide(Layer.mergeAll(mock.layer, Path.layer, HomeDirectoryLive, ConfigProviderLive))
233
+ )
234
+ )
235
+
236
+ expect(JSON.stringify(mock.store)).toContain("rotated-refresh")
237
+ })
238
+
178
239
  it("fails expired-token refresh when OAuth config is missing", async () => {
179
240
  await expect(
180
241
  run(