@knpkv/atlassian-common 0.2.0 → 0.3.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 +19 -0
- package/dist/Brand.d.ts +15 -15
- package/dist/Brand.d.ts.map +1 -1
- package/dist/Brand.js +5 -5
- package/dist/Brand.js.map +1 -1
- package/dist/Hash.d.ts +12 -14
- package/dist/Hash.d.ts.map +1 -1
- package/dist/Hash.js +18 -24
- package/dist/Hash.js.map +1 -1
- package/dist/ast/BlockNode.d.ts +120 -177
- package/dist/ast/BlockNode.d.ts.map +1 -1
- package/dist/ast/BlockNode.js +28 -8
- package/dist/ast/BlockNode.js.map +1 -1
- package/dist/ast/Document.d.ts +114 -156
- 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/InlineNode.d.ts +59 -247
- package/dist/ast/InlineNode.d.ts.map +1 -1
- package/dist/ast/InlineNode.js +28 -3
- package/dist/ast/InlineNode.js.map +1 -1
- package/dist/ast/MacroNode.d.ts +62 -102
- package/dist/ast/MacroNode.d.ts.map +1 -1
- package/dist/ast/MacroNode.js +12 -5
- package/dist/ast/MacroNode.js.map +1 -1
- package/dist/auth/OAuthOperations.d.ts +1 -1
- package/dist/auth/OAuthOperations.d.ts.map +1 -1
- package/dist/auth/OAuthOperations.js +5 -6
- package/dist/auth/OAuthOperations.js.map +1 -1
- package/dist/auth/OAuthResponseSchemas.d.ts +13 -13
- package/dist/config/ConfigPaths.d.ts +6 -26
- package/dist/config/ConfigPaths.d.ts.map +1 -1
- package/dist/config/ConfigPaths.js +23 -3
- package/dist/config/ConfigPaths.js.map +1 -1
- package/dist/config/OAuthSchemas.d.ts +24 -24
- package/dist/config/TokenStorage.d.ts +5 -24
- package/dist/config/TokenStorage.d.ts.map +1 -1
- package/dist/config/TokenStorage.js +35 -22
- package/dist/config/TokenStorage.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -9
- package/src/Brand.ts +5 -17
- package/src/Hash.ts +27 -27
- package/src/ast/BlockNode.ts +13 -13
- package/src/ast/Document.ts +1 -1
- package/src/ast/InlineNode.ts +5 -5
- package/src/ast/MacroNode.ts +7 -6
- package/src/auth/OAuthOperations.ts +5 -6
- package/src/config/ConfigPaths.ts +7 -7
- package/src/config/TokenStorage.ts +23 -27
- package/src/index.ts +1 -1
- package/test/Hash.test.ts +52 -0
- package/test/OAuthOperations.test.ts +2 -3
- package/tsconfig.tsbuildinfo +1 -1
package/src/ast/MacroNode.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
|
+
import * as Effect from "effect/Effect"
|
|
13
14
|
import * as Schema from "effect/Schema"
|
|
14
15
|
import { RawSource, SchemaVersion, SimpleBlockNode } from "./BlockNode.js"
|
|
15
16
|
|
|
@@ -25,7 +26,7 @@ export const PanelTypes = ["info", "warning", "note", "tip", "error", "panel"] a
|
|
|
25
26
|
*
|
|
26
27
|
* @category MacroNode
|
|
27
28
|
*/
|
|
28
|
-
export const PanelType = Schema.
|
|
29
|
+
export const PanelType = Schema.Literals(PanelTypes)
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Type for panel type.
|
|
@@ -118,8 +119,8 @@ export const CodeMacro = Schema.Struct({
|
|
|
118
119
|
language: Schema.optional(Schema.String),
|
|
119
120
|
title: Schema.optional(Schema.String),
|
|
120
121
|
code: Schema.String,
|
|
121
|
-
lineNumbers: Schema.
|
|
122
|
-
collapse: Schema.
|
|
122
|
+
lineNumbers: Schema.Boolean.pipe(Schema.withConstructorDefault(Effect.succeed(false))),
|
|
123
|
+
collapse: Schema.Boolean.pipe(Schema.withConstructorDefault(Effect.succeed(false))),
|
|
123
124
|
firstLine: Schema.optional(Schema.Number),
|
|
124
125
|
rawSource: RawSource
|
|
125
126
|
})
|
|
@@ -140,7 +141,7 @@ export const StatusMacro = Schema.Struct({
|
|
|
140
141
|
_tag: Schema.Literal("StatusMacro"),
|
|
141
142
|
version: SchemaVersion,
|
|
142
143
|
text: Schema.String,
|
|
143
|
-
color: Schema.
|
|
144
|
+
color: Schema.Literals(["Grey", "Red", "Yellow", "Green", "Blue"]),
|
|
144
145
|
rawSource: RawSource
|
|
145
146
|
})
|
|
146
147
|
|
|
@@ -156,13 +157,13 @@ export type StatusMacro = Schema.Schema.Type<typeof StatusMacro>
|
|
|
156
157
|
*
|
|
157
158
|
* @category MacroNode
|
|
158
159
|
*/
|
|
159
|
-
export const MacroNode = Schema.Union(
|
|
160
|
+
export const MacroNode = Schema.Union([
|
|
160
161
|
InfoPanel,
|
|
161
162
|
ExpandMacro,
|
|
162
163
|
TocMacro,
|
|
163
164
|
CodeMacro,
|
|
164
165
|
StatusMacro
|
|
165
|
-
)
|
|
166
|
+
])
|
|
166
167
|
|
|
167
168
|
/**
|
|
168
169
|
* Type for macro nodes.
|
|
@@ -18,10 +18,9 @@
|
|
|
18
18
|
*
|
|
19
19
|
* @module
|
|
20
20
|
*/
|
|
21
|
-
import * as HttpClient from "@effect/platform/HttpClient"
|
|
22
|
-
import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
|
|
23
21
|
import * as Effect from "effect/Effect"
|
|
24
22
|
import * as Schema from "effect/Schema"
|
|
23
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
25
24
|
import { type OAuthConfig, type OAuthToken } from "../config/OAuthSchemas.js"
|
|
26
25
|
import { ME_URL, RESOURCES_URL, REVOKE_URL, TOKEN_URL } from "./OAuthEndpoints.js"
|
|
27
26
|
import { OAuthError } from "./OAuthErrors.js"
|
|
@@ -83,7 +82,7 @@ export const exchangeCodeForTokens = (
|
|
|
83
82
|
|
|
84
83
|
const body = yield* response.json
|
|
85
84
|
|
|
86
|
-
return yield* Schema.
|
|
85
|
+
return yield* Schema.decodeUnknownEffect(TokenResponseSchema)(body)
|
|
87
86
|
}).pipe(
|
|
88
87
|
Effect.mapError((cause) => new OAuthError({ step: "token", cause }))
|
|
89
88
|
)
|
|
@@ -117,7 +116,7 @@ export const getAccessibleResources = (
|
|
|
117
116
|
|
|
118
117
|
const body = yield* response.json
|
|
119
118
|
|
|
120
|
-
return yield* Schema.
|
|
119
|
+
return yield* Schema.decodeUnknownEffect(Schema.Array(AccessibleResourceSchema))(body)
|
|
121
120
|
}).pipe(
|
|
122
121
|
Effect.mapError((cause) => new OAuthError({ step: "resources", cause }))
|
|
123
122
|
)
|
|
@@ -151,7 +150,7 @@ export const getUserInfo = (
|
|
|
151
150
|
|
|
152
151
|
const body = yield* response.json
|
|
153
152
|
|
|
154
|
-
return yield* Schema.
|
|
153
|
+
return yield* Schema.decodeUnknownEffect(UserInfoSchema)(body)
|
|
155
154
|
}).pipe(
|
|
156
155
|
Effect.mapError((cause) => new OAuthError({ step: "user-info", cause }))
|
|
157
156
|
)
|
|
@@ -197,7 +196,7 @@ export const refreshToken = (
|
|
|
197
196
|
const body = yield* response.json.pipe(
|
|
198
197
|
Effect.mapError((cause) => new OAuthError({ step: "refresh", cause }))
|
|
199
198
|
)
|
|
200
|
-
const tokenResponse = yield* Schema.
|
|
199
|
+
const tokenResponse = yield* Schema.decodeUnknownEffect(TokenResponseSchema)(body).pipe(
|
|
201
200
|
Effect.mapError((cause) => new OAuthError({ step: "refresh", cause }))
|
|
202
201
|
)
|
|
203
202
|
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
*
|
|
19
19
|
* @module
|
|
20
20
|
*/
|
|
21
|
-
import type * as Error from "@effect/platform/Error"
|
|
22
|
-
import * as FileSystem from "@effect/platform/FileSystem"
|
|
23
|
-
import * as Path from "@effect/platform/Path"
|
|
24
21
|
import * as Config from "effect/Config"
|
|
25
22
|
import * as Context from "effect/Context"
|
|
26
23
|
import * as Data from "effect/Data"
|
|
27
24
|
import * as Effect from "effect/Effect"
|
|
25
|
+
import * as FileSystem from "effect/FileSystem"
|
|
28
26
|
import * as Layer from "effect/Layer"
|
|
29
27
|
import * as Option from "effect/Option"
|
|
28
|
+
import * as Path from "effect/Path"
|
|
29
|
+
import type * as PlatformError from "effect/PlatformError"
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Error when home directory cannot be determined.
|
|
@@ -56,10 +56,10 @@ export interface HomeDirectory {
|
|
|
56
56
|
*
|
|
57
57
|
* @category Services
|
|
58
58
|
*/
|
|
59
|
-
export class HomeDirectoryTag extends Context.
|
|
59
|
+
export class HomeDirectoryTag extends Context.Service<
|
|
60
60
|
HomeDirectoryTag,
|
|
61
61
|
HomeDirectory
|
|
62
|
-
>() {}
|
|
62
|
+
>()("@knpkv/atlassian-common/HomeDirectory") {}
|
|
63
63
|
|
|
64
64
|
const HomeConfig = Config.option(Config.string("HOME")).pipe(
|
|
65
65
|
Config.orElse(() => Config.option(Config.string("USERPROFILE")))
|
|
@@ -145,7 +145,7 @@ export const ensureConfigDir = (
|
|
|
145
145
|
toolName: string
|
|
146
146
|
): Effect.Effect<
|
|
147
147
|
string,
|
|
148
|
-
HomeDirectoryError |
|
|
148
|
+
HomeDirectoryError | PlatformError.PlatformError,
|
|
149
149
|
FileSystem.FileSystem | Path.Path | HomeDirectoryTag
|
|
150
150
|
> =>
|
|
151
151
|
Effect.gen(function*() {
|
|
@@ -168,7 +168,7 @@ export const ensureConfigDir = (
|
|
|
168
168
|
export const writeSecureFile = (
|
|
169
169
|
filePath: string,
|
|
170
170
|
content: string
|
|
171
|
-
): Effect.Effect<void,
|
|
171
|
+
): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> =>
|
|
172
172
|
Effect.gen(function*() {
|
|
173
173
|
const fs = yield* FileSystem.FileSystem
|
|
174
174
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - **Load returns null, not error**: {@link loadToken} and {@link loadOAuthConfig} return
|
|
7
7
|
* `null` for missing/corrupt files — callers decide whether absence is an error.
|
|
8
8
|
* - **Save is atomic**: Writes go through {@link writeSecureFile} with `0o600` permissions.
|
|
9
|
-
* - **Schema-gated reads**: JSON is parsed then validated via `Schema.
|
|
9
|
+
* - **Schema-gated reads**: JSON is parsed then validated via `Schema.decodeUnknownEffect` —
|
|
10
10
|
* corrupt data is treated as absent rather than crashing.
|
|
11
11
|
*
|
|
12
12
|
* **Common tasks**
|
|
@@ -17,16 +17,24 @@
|
|
|
17
17
|
*
|
|
18
18
|
* @module
|
|
19
19
|
*/
|
|
20
|
-
import type * as Error from "@effect/platform/Error"
|
|
21
|
-
import * as FileSystem from "@effect/platform/FileSystem"
|
|
22
|
-
import type * as Path from "@effect/platform/Path"
|
|
23
20
|
import * as Data from "effect/Data"
|
|
24
21
|
import * as Effect from "effect/Effect"
|
|
22
|
+
import * as FileSystem from "effect/FileSystem"
|
|
23
|
+
import type * as Path from "effect/Path"
|
|
24
|
+
import type * as PlatformError from "effect/PlatformError"
|
|
25
25
|
import * as Schema from "effect/Schema"
|
|
26
26
|
import { type HomeDirectoryError, type HomeDirectoryTag } from "./ConfigPaths.js"
|
|
27
27
|
import { ensureConfigDir, getAuthPath, getOAuthConfigPath, writeSecureFile } from "./ConfigPaths.js"
|
|
28
28
|
import { type OAuthConfig, OAuthConfigSchema, type OAuthToken, OAuthTokenSchema } from "./OAuthSchemas.js"
|
|
29
29
|
|
|
30
|
+
const parseJsonOrNull = (content: string): unknown | null => {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(content) as unknown
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
/**
|
|
31
39
|
* Error during file system operations.
|
|
32
40
|
*
|
|
@@ -62,7 +70,7 @@ export const loadToken = (
|
|
|
62
70
|
const tokenPath = yield* getAuthPath(toolName)
|
|
63
71
|
|
|
64
72
|
const exists = yield* fs.exists(tokenPath).pipe(
|
|
65
|
-
Effect.
|
|
73
|
+
Effect.catchCause(() => Effect.succeed(false))
|
|
66
74
|
)
|
|
67
75
|
if (!exists) {
|
|
68
76
|
return null
|
|
@@ -72,20 +80,14 @@ export const loadToken = (
|
|
|
72
80
|
Effect.mapError((cause) => new FileSystemError({ operation: "read", path: tokenPath, cause }))
|
|
73
81
|
)
|
|
74
82
|
|
|
75
|
-
const parsed = yield* Effect.sync(() =>
|
|
76
|
-
try {
|
|
77
|
-
return JSON.parse(content) as unknown
|
|
78
|
-
} catch {
|
|
79
|
-
return null
|
|
80
|
-
}
|
|
81
|
-
})
|
|
83
|
+
const parsed = yield* Effect.sync(() => parseJsonOrNull(content))
|
|
82
84
|
if (parsed === null) {
|
|
83
85
|
yield* Effect.logWarning(`Corrupted auth.json at ${tokenPath} — could not parse JSON`)
|
|
84
86
|
return null
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
return yield* Schema.
|
|
88
|
-
Effect.
|
|
89
|
+
return yield* Schema.decodeUnknownEffect(OAuthTokenSchema)(parsed).pipe(
|
|
90
|
+
Effect.catchCause((e) =>
|
|
89
91
|
Effect.logWarning(`Invalid token schema in ${tokenPath}: ${e}`).pipe(
|
|
90
92
|
Effect.map(() => null)
|
|
91
93
|
)
|
|
@@ -106,7 +108,7 @@ export const saveToken = (
|
|
|
106
108
|
token: OAuthToken
|
|
107
109
|
): Effect.Effect<
|
|
108
110
|
void,
|
|
109
|
-
FileSystemError | HomeDirectoryError |
|
|
111
|
+
FileSystemError | HomeDirectoryError | PlatformError.PlatformError,
|
|
110
112
|
FileSystem.FileSystem | Path.Path | HomeDirectoryTag
|
|
111
113
|
> =>
|
|
112
114
|
Effect.gen(function*() {
|
|
@@ -134,7 +136,7 @@ export const deleteToken = (
|
|
|
134
136
|
const tokenPath = yield* getAuthPath(toolName)
|
|
135
137
|
|
|
136
138
|
yield* fs.remove(tokenPath).pipe(
|
|
137
|
-
Effect.
|
|
139
|
+
Effect.catchCause(() => Effect.void)
|
|
138
140
|
)
|
|
139
141
|
})
|
|
140
142
|
|
|
@@ -158,7 +160,7 @@ export const loadOAuthConfig = (
|
|
|
158
160
|
const configPath = yield* getOAuthConfigPath(toolName)
|
|
159
161
|
|
|
160
162
|
const exists = yield* fs.exists(configPath).pipe(
|
|
161
|
-
Effect.
|
|
163
|
+
Effect.catchCause(() => Effect.succeed(false))
|
|
162
164
|
)
|
|
163
165
|
if (!exists) {
|
|
164
166
|
return null
|
|
@@ -168,20 +170,14 @@ export const loadOAuthConfig = (
|
|
|
168
170
|
Effect.mapError((cause) => new FileSystemError({ operation: "read", path: configPath, cause }))
|
|
169
171
|
)
|
|
170
172
|
|
|
171
|
-
const parsed = yield* Effect.sync(() =>
|
|
172
|
-
try {
|
|
173
|
-
return JSON.parse(content) as unknown
|
|
174
|
-
} catch {
|
|
175
|
-
return null
|
|
176
|
-
}
|
|
177
|
-
})
|
|
173
|
+
const parsed = yield* Effect.sync(() => parseJsonOrNull(content))
|
|
178
174
|
if (parsed === null) {
|
|
179
175
|
yield* Effect.logWarning(`Corrupted oauth config at ${configPath} — could not parse JSON`)
|
|
180
176
|
return null
|
|
181
177
|
}
|
|
182
178
|
|
|
183
|
-
return yield* Schema.
|
|
184
|
-
Effect.
|
|
179
|
+
return yield* Schema.decodeUnknownEffect(OAuthConfigSchema)(parsed).pipe(
|
|
180
|
+
Effect.catchCause((e) =>
|
|
185
181
|
Effect.logWarning(`Invalid OAuth config schema in ${configPath}: ${e}`).pipe(
|
|
186
182
|
Effect.map(() => null)
|
|
187
183
|
)
|
|
@@ -202,7 +198,7 @@ export const saveOAuthConfig = (
|
|
|
202
198
|
config: OAuthConfig
|
|
203
199
|
): Effect.Effect<
|
|
204
200
|
void,
|
|
205
|
-
FileSystemError | HomeDirectoryError |
|
|
201
|
+
FileSystemError | HomeDirectoryError | PlatformError.PlatformError,
|
|
206
202
|
FileSystem.FileSystem | Path.Path | HomeDirectoryTag
|
|
207
203
|
> =>
|
|
208
204
|
Effect.gen(function*() {
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ export {
|
|
|
19
19
|
} from "./Brand.js"
|
|
20
20
|
|
|
21
21
|
// Hash utilities
|
|
22
|
-
export { hashBuffer, hashContent,
|
|
22
|
+
export { hashBuffer, hashContent, hashEquals } from "./Hash.js"
|
|
23
23
|
|
|
24
24
|
// Error types
|
|
25
25
|
export { ParseError, SerializeError } from "./SerializeError.js"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { it as effectIt } from "@effect/vitest"
|
|
2
|
+
import * as Crypto from "effect/Crypto"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import { describe, expect, it } from "vitest"
|
|
5
|
+
import { ContentHash } from "../src/Brand.js"
|
|
6
|
+
import { hashBuffer, hashContent, hashEquals } from "../src/Hash.js"
|
|
7
|
+
|
|
8
|
+
const byteSequenceDigest = Uint8Array.from({ length: 32 }, (_, index) => index)
|
|
9
|
+
const byteSequenceHash = ContentHash("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
|
|
10
|
+
|
|
11
|
+
describe("Hash", () => {
|
|
12
|
+
effectIt.effect("delegates SHA-256 hashing to the provided Crypto service", () =>
|
|
13
|
+
Effect.gen(function*() {
|
|
14
|
+
const hash = yield* hashContent("hello")
|
|
15
|
+
expect(hash).toBe(byteSequenceHash)
|
|
16
|
+
}).pipe(
|
|
17
|
+
Effect.provideService(
|
|
18
|
+
Crypto.Crypto,
|
|
19
|
+
Crypto.make({
|
|
20
|
+
randomBytes: (size) => new Uint8Array(size),
|
|
21
|
+
digest: (algorithm, data) => {
|
|
22
|
+
expect(algorithm).toBe("SHA-256")
|
|
23
|
+
expect(Array.from(data)).toEqual([104, 101, 108, 108, 111])
|
|
24
|
+
return Effect.succeed(byteSequenceDigest)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
))
|
|
29
|
+
|
|
30
|
+
effectIt.effect("hashes binary content through the provided Crypto service", () =>
|
|
31
|
+
Effect.gen(function*() {
|
|
32
|
+
const hash = yield* hashBuffer(Uint8Array.of(1, 2, 3))
|
|
33
|
+
expect(hash).toBe(byteSequenceHash)
|
|
34
|
+
}).pipe(
|
|
35
|
+
Effect.provideService(
|
|
36
|
+
Crypto.Crypto,
|
|
37
|
+
Crypto.make({
|
|
38
|
+
randomBytes: (size) => new Uint8Array(size),
|
|
39
|
+
digest: (algorithm, data) => {
|
|
40
|
+
expect(algorithm).toBe("SHA-256")
|
|
41
|
+
expect(Array.from(data)).toEqual([1, 2, 3])
|
|
42
|
+
return Effect.succeed(byteSequenceDigest)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
))
|
|
47
|
+
|
|
48
|
+
it("compares content hashes by value", () => {
|
|
49
|
+
expect(hashEquals(byteSequenceHash, byteSequenceHash)).toBe(true)
|
|
50
|
+
expect(hashEquals(byteSequenceHash, ContentHash("f".repeat(64)))).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import * as HttpClient from "@effect/platform/HttpClient"
|
|
2
|
-
import type * as HttpClientRequest from "@effect/platform/HttpClientRequest"
|
|
3
|
-
import * as HttpClientResponse from "@effect/platform/HttpClientResponse"
|
|
4
1
|
import { describe, it } from "@effect/vitest"
|
|
5
2
|
import * as Effect from "effect/Effect"
|
|
6
3
|
import * as Layer from "effect/Layer"
|
|
4
|
+
import type { HttpClientRequest } from "effect/unstable/http"
|
|
5
|
+
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
|
7
6
|
import { expect } from "vitest"
|
|
8
7
|
import { OAuthError } from "../src/auth/OAuthErrors.js"
|
|
9
8
|
import {
|