@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.
- package/CHANGELOG.md +72 -0
- package/README.md +1 -1
- package/dist/Brand.d.ts +2 -2
- package/dist/Brand.d.ts.map +1 -1
- package/dist/Brand.js.map +1 -1
- package/dist/Hash.d.ts.map +1 -1
- package/dist/Hash.js +2 -2
- package/dist/Hash.js.map +1 -1
- package/dist/ast/Document.d.ts +3 -19
- package/dist/ast/Document.d.ts.map +1 -1
- package/dist/ast/Document.js +4 -3
- package/dist/ast/Document.js.map +1 -1
- package/dist/ast/MacroNode.d.ts +3 -3
- package/dist/attachments.d.ts.map +1 -1
- package/dist/attachments.js +2 -6
- package/dist/attachments.js.map +1 -1
- package/dist/auth/OAuthEndpoints.d.ts +13 -0
- package/dist/auth/OAuthEndpoints.d.ts.map +1 -1
- package/dist/auth/OAuthEndpoints.js +24 -3
- package/dist/auth/OAuthEndpoints.js.map +1 -1
- package/dist/auth/OAuthErrors.d.ts +22 -0
- package/dist/auth/OAuthErrors.d.ts.map +1 -1
- package/dist/auth/OAuthErrors.js +1 -1
- package/dist/auth/OAuthErrors.js.map +1 -1
- package/dist/auth/OAuthOperations.d.ts +8 -2
- package/dist/auth/OAuthOperations.d.ts.map +1 -1
- package/dist/auth/OAuthOperations.js +18 -8
- package/dist/auth/OAuthOperations.js.map +1 -1
- package/dist/auth/OAuthResponseSchemas.d.ts +11 -0
- package/dist/auth/OAuthResponseSchemas.d.ts.map +1 -1
- package/dist/auth/OAuthResponseSchemas.js +11 -0
- package/dist/auth/OAuthResponseSchemas.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/bin.js +2 -2
- package/dist/bin.js.map +1 -1
- package/dist/config/AuthProfiles.d.ts +140 -175
- package/dist/config/AuthProfiles.d.ts.map +1 -1
- package/dist/config/AuthProfiles.js +2 -1
- package/dist/config/AuthProfiles.js.map +1 -1
- package/dist/config/ConfigPaths.d.ts.map +1 -1
- package/dist/config/ConfigPaths.js +2 -2
- package/dist/config/ProfileManager.d.ts +14 -3
- package/dist/config/ProfileManager.d.ts.map +1 -1
- package/dist/config/ProfileManager.js +33 -5
- package/dist/config/ProfileManager.js.map +1 -1
- package/dist/config/TokenStorage.d.ts.map +1 -1
- package/dist/config/TokenStorage.js +2 -1
- package/dist/config/TokenStorage.js.map +1 -1
- package/dist/serializers/MarkdownSerializer.d.ts.map +1 -1
- package/dist/serializers/MarkdownSerializer.js.map +1 -1
- package/package.json +8 -8
- package/src/Brand.ts +5 -5
- package/src/Hash.ts +2 -2
- package/src/ast/Document.ts +5 -4
- package/src/attachments.ts +2 -1
- package/src/auth/OAuthEndpoints.ts +25 -3
- package/src/auth/OAuthErrors.ts +23 -1
- package/src/auth/OAuthOperations.ts +33 -20
- package/src/auth/OAuthResponseSchemas.ts +12 -0
- package/src/auth/index.ts +1 -0
- package/src/bin.ts +5 -3
- package/src/config/AuthProfiles.ts +4 -2
- package/src/config/ConfigPaths.ts +3 -3
- package/src/config/ProfileManager.ts +42 -6
- package/src/config/TokenStorage.ts +4 -2
- package/test/ApiUpdateWorkflows.test.ts +356 -0
- package/test/OAuthEndpoints.test.ts +23 -0
- package/test/OAuthOperations.test.ts +2 -2
- 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(
|