@knpkv/atlassian-common 0.4.0 → 1.0.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.
@@ -1,25 +1,30 @@
1
+ import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
1
2
  import { it as effectIt } from "@effect/vitest"
2
3
  import * as Effect from "effect/Effect"
3
4
  import { describe, expect, it } from "vitest"
4
5
  import { AUTH_URL, buildAuthUrl, computeCodeChallenge, generateCodeVerifier } from "../src/auth/OAuthEndpoints.js"
5
6
 
7
+ const withCrypto = <A, E, R>(effect: Effect.Effect<A, E, R>) => effect.pipe(Effect.provide(NodeCrypto.layer))
8
+
6
9
  describe("OAuthEndpoints", () => {
7
10
  // ---- PKCE ----
8
11
 
9
12
  describe("generateCodeVerifier", () => {
10
13
  // 32 random bytes base64url-encoded = 43 chars; verifies output shape for RFC 7636 compliance
11
- it("returns a base64url string of 43 characters (32 random bytes)", () => {
12
- const verifier = generateCodeVerifier()
13
- expect(verifier).toHaveLength(43)
14
- expect(verifier).toMatch(/^[A-Za-z0-9_-]+$/)
15
- })
14
+ effectIt.effect("returns a base64url string of 43 characters (32 random bytes)", () =>
15
+ Effect.gen(function*() {
16
+ const verifier = yield* generateCodeVerifier()
17
+ expect(verifier).toHaveLength(43)
18
+ expect(verifier).toMatch(/^[A-Za-z0-9_-]+$/)
19
+ }).pipe(withCrypto))
16
20
 
17
21
  // PKCE verifiers must be unique per auth request — reuse enables replay attacks
18
- it("produces unique values", () => {
19
- const a = generateCodeVerifier()
20
- const b = generateCodeVerifier()
21
- expect(a).not.toBe(b)
22
- })
22
+ effectIt.effect("produces unique values", () =>
23
+ Effect.gen(function*() {
24
+ const a = yield* generateCodeVerifier()
25
+ const b = yield* generateCodeVerifier()
26
+ expect(a).not.toBe(b)
27
+ }).pipe(withCrypto))
23
28
  })
24
29
 
25
30
  describe("computeCodeChallenge", () => {
@@ -30,23 +35,23 @@ describe("OAuthEndpoints", () => {
30
35
  const expected = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
31
36
  const challenge = yield* computeCodeChallenge(verifier)
32
37
  expect(challenge).toBe(expected)
33
- }))
38
+ }).pipe(withCrypto))
34
39
 
35
40
  // Same verifier must always produce same challenge — server verifies this on token exchange
36
41
  effectIt.effect("is deterministic", () =>
37
42
  Effect.gen(function*() {
38
- const v = generateCodeVerifier()
43
+ const v = yield* generateCodeVerifier()
39
44
  const a = yield* computeCodeChallenge(v)
40
45
  const b = yield* computeCodeChallenge(v)
41
46
  expect(a).toBe(b)
42
- }))
47
+ }).pipe(withCrypto))
43
48
 
44
49
  // base64url must not contain +, /, = — these break URL query params
45
50
  effectIt.effect("produces base64url output (no +, /, =)", () =>
46
51
  Effect.gen(function*() {
47
52
  const challenge = yield* computeCodeChallenge("test-verifier-string")
48
53
  expect(challenge).toMatch(/^[A-Za-z0-9_-]+$/)
49
- }))
54
+ }).pipe(withCrypto))
50
55
  })
51
56
 
52
57
  // ---- buildAuthUrl ----
@@ -103,7 +108,7 @@ describe("OAuthEndpoints", () => {
103
108
  const parsed = new URL(url)
104
109
  expect(parsed.searchParams.get("code_challenge")).toBe(challenge)
105
110
  expect(parsed.searchParams.get("code_challenge_method")).toBe("S256")
106
- })
111
+ }).pipe(withCrypto)
107
112
  )
108
113
  })
109
114
  })
@@ -1,6 +1,7 @@
1
1
  import { describe, it } from "@effect/vitest"
2
2
  import * as Effect from "effect/Effect"
3
3
  import * as Layer from "effect/Layer"
4
+ import { TestClock } from "effect/testing"
4
5
  import type { HttpClientRequest } from "effect/unstable/http"
5
6
  import { HttpClient, HttpClientResponse } from "effect/unstable/http"
6
7
  import { expect } from "vitest"
@@ -58,6 +59,8 @@ const oauthConfig: OAuthConfig = {
58
59
  clientSecret: "csecret"
59
60
  }
60
61
 
62
+ const TEST_NOW = Date.parse("2026-01-01T00:00:00.000Z")
63
+
61
64
  const oauthToken: OAuthToken = {
62
65
  access_token: "old-access",
63
66
  refresh_token: "old-refresh",
@@ -221,6 +224,7 @@ describe("OAuthOperations", () => {
221
224
  // Verifies grant_type=refresh_token is sent and returned token preserves cloud_id/site_url from original
222
225
  it.effect("sends refresh_token and returns updated OAuthToken", () =>
223
226
  Effect.gen(function*() {
227
+ yield* TestClock.setTime(TEST_NOW)
224
228
  const { capturedRequests, mockClient } = createMockHttpClient([
225
229
  { status: 200, body: { ...validTokenBody, access_token: "new-access", refresh_token: "new-refresh" } }
226
230
  ])
@@ -232,7 +236,7 @@ describe("OAuthOperations", () => {
232
236
  // preserves existing fields
233
237
  expect(result.cloud_id).toBe("cloud-1")
234
238
  expect(result.site_url).toBe("https://site.atlassian.net")
235
- expect(result.expires_at).toBeGreaterThan(Date.now())
239
+ expect(result.expires_at).toBe(TEST_NOW + validTokenBody.expires_in * 1000)
236
240
 
237
241
  const req = capturedRequests[0]!
238
242
  const body = yield* req.body._tag === "Uint8Array"