@knpkv/atlassian-common 0.3.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.
- package/CHANGELOG.md +23 -0
- package/README.md +29 -13
- package/dist/Hash.d.ts +4 -4
- package/dist/Hash.js +6 -6
- package/dist/Hash.js.map +1 -1
- package/dist/auth/OAuthEndpoints.d.ts +6 -4
- package/dist/auth/OAuthEndpoints.d.ts.map +1 -1
- package/dist/auth/OAuthEndpoints.js +12 -7
- package/dist/auth/OAuthEndpoints.js.map +1 -1
- package/dist/auth/OAuthErrors.d.ts.map +1 -1
- package/dist/auth/OAuthErrors.js +4 -3
- package/dist/auth/OAuthErrors.js.map +1 -1
- package/dist/auth/OAuthOperations.d.ts +0 -20
- package/dist/auth/OAuthOperations.d.ts.map +1 -1
- package/dist/auth/OAuthOperations.js +3 -1
- package/dist/auth/OAuthOperations.js.map +1 -1
- package/dist/auth/uuid.d.ts +5 -3
- package/dist/auth/uuid.d.ts.map +1 -1
- package/dist/auth/uuid.js +7 -3
- package/dist/auth/uuid.js.map +1 -1
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +58 -0
- package/dist/bin.js.map +1 -0
- package/dist/config/AuthProfiles.d.ts +342 -0
- package/dist/config/AuthProfiles.d.ts.map +1 -0
- package/dist/config/AuthProfiles.js +275 -0
- package/dist/config/AuthProfiles.js.map +1 -0
- package/dist/config/ConfigPaths.d.ts +6 -0
- package/dist/config/ConfigPaths.d.ts.map +1 -1
- package/dist/config/ConfigPaths.js +10 -0
- package/dist/config/ConfigPaths.js.map +1 -1
- package/dist/config/ProfileManager.d.ts +92 -0
- package/dist/config/ProfileManager.d.ts.map +1 -0
- package/dist/config/ProfileManager.js +175 -0
- package/dist/config/ProfileManager.js.map +1 -0
- package/dist/config/index.d.ts +3 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +5 -1
- package/dist/config/index.js.map +1 -1
- package/package.json +11 -3
- package/src/Hash.ts +6 -6
- package/src/auth/OAuthEndpoints.ts +18 -11
- package/src/auth/OAuthErrors.ts +4 -3
- package/src/auth/OAuthOperations.ts +3 -1
- package/src/auth/uuid.ts +9 -3
- package/src/bin.ts +115 -0
- package/src/config/AuthProfiles.ts +384 -0
- package/src/config/ConfigPaths.ts +14 -0
- package/src/config/ProfileManager.ts +268 -0
- package/src/config/index.ts +42 -0
- package/test/AuthProfiles.test.ts +131 -0
- package/test/OAuthEndpoints.test.ts +20 -15
- package/test/OAuthOperations.test.ts +5 -1
- package/test/ProfileManager.test.ts +188 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/bin.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { NodeFileSystem, NodeHttpClient, NodePath, NodeRuntime, NodeServices, NodeStdio } from "@effect/platform-node"
|
|
3
|
+
import * as Console from "effect/Console"
|
|
4
|
+
import * as Effect from "effect/Effect"
|
|
5
|
+
import * as Layer from "effect/Layer"
|
|
6
|
+
import * as Stdio from "effect/Stdio"
|
|
7
|
+
import { Argument as Args, Command } from "effect/unstable/cli"
|
|
8
|
+
import pkg from "../package.json" with { type: "json" }
|
|
9
|
+
import {
|
|
10
|
+
HomeDirectoryLive,
|
|
11
|
+
inspectAllToolProfiles,
|
|
12
|
+
migrateLegacyProfiles,
|
|
13
|
+
refreshActiveProfiles,
|
|
14
|
+
type ToolProfileStatus,
|
|
15
|
+
useProfileForAllTools
|
|
16
|
+
} from "./config/index.js"
|
|
17
|
+
|
|
18
|
+
const profileArg = Args.string("profile").pipe(
|
|
19
|
+
Args.withDescription("Profile ID, name, site URL, cloud ID, or account ID")
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const printStatuses = (statuses: ReadonlyArray<ToolProfileStatus>) =>
|
|
23
|
+
Effect.gen(function*() {
|
|
24
|
+
for (const status of statuses) {
|
|
25
|
+
const active = status.activeProfile
|
|
26
|
+
yield* Console.log(`${status.tool.label} (${status.tool.toolName})`)
|
|
27
|
+
if (status.authStoreName !== status.tool.toolName) {
|
|
28
|
+
yield* Console.log(` Auth store: ${status.authStoreName}`)
|
|
29
|
+
}
|
|
30
|
+
yield* Console.log(` Active: ${active ? active.name : "none"}`)
|
|
31
|
+
if (active) {
|
|
32
|
+
yield* Console.log(` Profile ID: ${active.id}`)
|
|
33
|
+
yield* Console.log(` Site: ${active.token.site_url}`)
|
|
34
|
+
yield* Console.log(` Token: ${status.tokenStatus}`)
|
|
35
|
+
yield* Console.log(
|
|
36
|
+
` Scopes: ${status.missingScopes.length === 0 ? "ok" : `missing ${status.missingScopes.join(", ")}`}`
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const profilesList = Command.make("list", {}, () => inspectAllToolProfiles().pipe(Effect.flatMap(printStatuses)))
|
|
43
|
+
.pipe(Command.withDescription("List active Atlassian profiles for all tools"))
|
|
44
|
+
|
|
45
|
+
const profilesUse = Command.make(
|
|
46
|
+
"use",
|
|
47
|
+
{ profile: profileArg },
|
|
48
|
+
({ profile }) => useProfileForAllTools(profile).pipe(Effect.flatMap(printStatuses))
|
|
49
|
+
).pipe(Command.withDescription("Use a profile across Atlassian tools"))
|
|
50
|
+
|
|
51
|
+
const profilesDoctor = Command.make("doctor", {}, () =>
|
|
52
|
+
inspectAllToolProfiles().pipe(
|
|
53
|
+
Effect.flatMap((statuses) =>
|
|
54
|
+
Effect.gen(function*() {
|
|
55
|
+
yield* printStatuses(statuses)
|
|
56
|
+
for (const status of statuses) {
|
|
57
|
+
if (!status.activeProfile) {
|
|
58
|
+
yield* Console.log(`Suggestion: run '${status.tool.loginHint}' for ${status.tool.label}.`)
|
|
59
|
+
}
|
|
60
|
+
if (status.tokenStatus === "expired") {
|
|
61
|
+
yield* Console.log(
|
|
62
|
+
`Suggestion: run 'atlassian auth refresh' or '${status.tool.loginHint}' for ${status.tool.label}.`
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
if (status.missingScopes.length > 0) {
|
|
66
|
+
yield* Console.log(
|
|
67
|
+
`Suggestion: re-login with ${status.tool.label}; missing scopes: ${status.missingScopes.join(", ")}.`
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
)).pipe(Command.withDescription("Diagnose tokens, scopes, and active profile usage"))
|
|
74
|
+
|
|
75
|
+
const profilesMigrate = Command.make("migrate", {}, () => migrateLegacyProfiles().pipe(Effect.flatMap(printStatuses)))
|
|
76
|
+
.pipe(Command.withDescription("Migrate legacy auth.json files into shared profiles.json storage"))
|
|
77
|
+
|
|
78
|
+
const profiles = Command.make("profiles").pipe(
|
|
79
|
+
Command.withDescription("Manage shared Atlassian auth profiles"),
|
|
80
|
+
Command.withSubcommands([profilesList, profilesUse, profilesDoctor, profilesMigrate])
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const authRefresh = Command.make("refresh", {}, () => refreshActiveProfiles().pipe(Effect.flatMap(printStatuses)))
|
|
84
|
+
.pipe(Command.withDescription("Refresh expired active OAuth tokens"))
|
|
85
|
+
|
|
86
|
+
const auth = Command.make("auth").pipe(
|
|
87
|
+
Command.withDescription("Manage Atlassian authentication"),
|
|
88
|
+
Command.withSubcommands([authRefresh])
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const atlassian = Command.make("atlassian", {}, () => Console.log("Usage: atlassian profiles|auth")).pipe(
|
|
92
|
+
Command.withDescription("Unified Atlassian profile manager"),
|
|
93
|
+
Command.withSubcommands([profiles, auth])
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const cli = Command.runWith(atlassian, { version: pkg.version })
|
|
97
|
+
const program = Effect.gen(function*() {
|
|
98
|
+
const stdio = yield* Stdio.Stdio
|
|
99
|
+
const args = yield* stdio.args
|
|
100
|
+
return yield* cli(args)
|
|
101
|
+
}).pipe(
|
|
102
|
+
Effect.provide(NodeStdio.layer),
|
|
103
|
+
Effect.provide(
|
|
104
|
+
Layer.mergeAll(
|
|
105
|
+
NodeServices.layer,
|
|
106
|
+
NodeFileSystem.layer,
|
|
107
|
+
NodePath.layer,
|
|
108
|
+
NodeHttpClient.layerFetch,
|
|
109
|
+
HomeDirectoryLive
|
|
110
|
+
)
|
|
111
|
+
),
|
|
112
|
+
Effect.catch((error: unknown) => Console.error(String(error)).pipe(Effect.andThen(Effect.fail(error))))
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
NodeRuntime.runMain(program)
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-account/site auth profile storage for Atlassian CLIs.
|
|
3
|
+
*
|
|
4
|
+
* **Mental model**
|
|
5
|
+
*
|
|
6
|
+
* - `profiles.json` is the source of truth for multiple logged-in accounts/sites.
|
|
7
|
+
* - The active profile is mirrored to the legacy `auth.json` token file so older
|
|
8
|
+
* single-profile consumers keep working.
|
|
9
|
+
* - Existing single-token installs are treated as a one-profile store on read.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import * as Clock from "effect/Clock"
|
|
14
|
+
import * as Effect from "effect/Effect"
|
|
15
|
+
import * as FileSystem from "effect/FileSystem"
|
|
16
|
+
import type * as Path from "effect/Path"
|
|
17
|
+
import * as Schema from "effect/Schema"
|
|
18
|
+
import {
|
|
19
|
+
ensureConfigDir,
|
|
20
|
+
getProfilesPath,
|
|
21
|
+
type HomeDirectoryError,
|
|
22
|
+
type HomeDirectoryTag,
|
|
23
|
+
writeSecureFile
|
|
24
|
+
} from "./ConfigPaths.js"
|
|
25
|
+
import { type OAuthToken, OAuthTokenSchema } from "./OAuthSchemas.js"
|
|
26
|
+
import { deleteToken, FileSystemError, loadToken, saveToken } from "./TokenStorage.js"
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A persisted Atlassian auth profile.
|
|
30
|
+
*
|
|
31
|
+
* @category Schema
|
|
32
|
+
*/
|
|
33
|
+
export const AuthProfileSchema = Schema.Struct({
|
|
34
|
+
/** Stable identifier derived from account and cloud IDs. */
|
|
35
|
+
id: Schema.String,
|
|
36
|
+
/** Human-readable profile label. */
|
|
37
|
+
name: Schema.String,
|
|
38
|
+
/** OAuth token and selected site metadata. */
|
|
39
|
+
token: OAuthTokenSchema,
|
|
40
|
+
/** ISO timestamp when the profile was first created. */
|
|
41
|
+
created_at: Schema.String,
|
|
42
|
+
/** ISO timestamp when the profile was last updated. */
|
|
43
|
+
updated_at: Schema.String
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Type for a persisted Atlassian auth profile.
|
|
48
|
+
*
|
|
49
|
+
* @category Types
|
|
50
|
+
*/
|
|
51
|
+
export type AuthProfile = Schema.Schema.Type<typeof AuthProfileSchema>
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Schema for the profile registry file.
|
|
55
|
+
*
|
|
56
|
+
* @category Schema
|
|
57
|
+
*/
|
|
58
|
+
export const AuthProfilesFileSchema = Schema.Struct({
|
|
59
|
+
/** Active profile ID. Omitted when no profile is active. */
|
|
60
|
+
activeProfileId: Schema.optional(Schema.String),
|
|
61
|
+
/** Known auth profiles for this CLI. */
|
|
62
|
+
profiles: Schema.Array(AuthProfileSchema)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Type for the profile registry file.
|
|
67
|
+
*
|
|
68
|
+
* @category Types
|
|
69
|
+
*/
|
|
70
|
+
export type AuthProfilesFile = Schema.Schema.Type<typeof AuthProfilesFileSchema>
|
|
71
|
+
|
|
72
|
+
const emptyProfiles = (): AuthProfilesFile => ({ profiles: [] })
|
|
73
|
+
|
|
74
|
+
const parseJsonOrNull = (content: string): unknown | null => {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(content) as unknown
|
|
77
|
+
} catch {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const hostFromUrl = (url: string): string => {
|
|
83
|
+
try {
|
|
84
|
+
return new URL(url).host
|
|
85
|
+
} catch {
|
|
86
|
+
return url
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const normalizeProfileIdPart = (value: string): string =>
|
|
91
|
+
value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "")
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build a stable profile ID from token site and user identity.
|
|
95
|
+
*
|
|
96
|
+
* @category Utilities
|
|
97
|
+
*/
|
|
98
|
+
export const profileIdFromToken = (token: OAuthToken): string => {
|
|
99
|
+
const account = token.user?.account_id ? normalizeProfileIdPart(token.user.account_id) : "unknown-account"
|
|
100
|
+
const site = normalizeProfileIdPart(token.cloud_id || hostFromUrl(token.site_url))
|
|
101
|
+
return `${account}@${site}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build a readable profile label from token site and user identity.
|
|
106
|
+
*
|
|
107
|
+
* @category Utilities
|
|
108
|
+
*/
|
|
109
|
+
export const profileNameFromToken = (token: OAuthToken): string => {
|
|
110
|
+
const site = hostFromUrl(token.site_url)
|
|
111
|
+
if (!token.user) return site
|
|
112
|
+
const email = token.user.email.length > 0 ? ` (${token.user.email})` : ""
|
|
113
|
+
return `${token.user.name}${email} @ ${site}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create a profile record from an OAuth token.
|
|
118
|
+
*
|
|
119
|
+
* @category Utilities
|
|
120
|
+
*/
|
|
121
|
+
export const profileFromToken = (token: OAuthToken, now: Date): AuthProfile => {
|
|
122
|
+
const timestamp = now.toISOString()
|
|
123
|
+
return {
|
|
124
|
+
id: profileIdFromToken(token),
|
|
125
|
+
name: profileNameFromToken(token),
|
|
126
|
+
token,
|
|
127
|
+
created_at: timestamp,
|
|
128
|
+
updated_at: timestamp
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const activeProfileFromStore = (store: AuthProfilesFile): AuthProfile | null => {
|
|
133
|
+
if (store.profiles.length === 0) return null
|
|
134
|
+
if (!store.activeProfileId) return store.profiles[0] ?? null
|
|
135
|
+
return store.profiles.find((profile) => profile.id === store.activeProfileId) ?? store.profiles[0] ?? null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const storeFromLegacyToken = (token: OAuthToken | null, now: Date): AuthProfilesFile =>
|
|
139
|
+
token === null
|
|
140
|
+
? emptyProfiles()
|
|
141
|
+
: {
|
|
142
|
+
activeProfileId: profileIdFromToken(token),
|
|
143
|
+
profiles: [profileFromToken(token, now)]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const readProfilesFile = (
|
|
147
|
+
toolName: string
|
|
148
|
+
): Effect.Effect<
|
|
149
|
+
AuthProfilesFile | null,
|
|
150
|
+
FileSystemError | HomeDirectoryError,
|
|
151
|
+
FileSystem.FileSystem | Path.Path | HomeDirectoryTag
|
|
152
|
+
> =>
|
|
153
|
+
Effect.gen(function*() {
|
|
154
|
+
const fs = yield* FileSystem.FileSystem
|
|
155
|
+
const profilesPath = yield* getProfilesPath(toolName)
|
|
156
|
+
|
|
157
|
+
const exists = yield* fs.exists(profilesPath).pipe(
|
|
158
|
+
Effect.catch(() => Effect.succeed(false))
|
|
159
|
+
)
|
|
160
|
+
if (!exists) return null
|
|
161
|
+
|
|
162
|
+
const content = yield* fs.readFileString(profilesPath).pipe(
|
|
163
|
+
Effect.mapError((cause) => new FileSystemError({ operation: "read", path: profilesPath, cause }))
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
const parsed = parseJsonOrNull(content)
|
|
167
|
+
if (parsed === null) {
|
|
168
|
+
yield* Effect.logWarning(`Corrupted profiles.json at ${profilesPath} - could not parse JSON`)
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return yield* Schema.decodeUnknownEffect(AuthProfilesFileSchema)(parsed).pipe(
|
|
173
|
+
Effect.catch((e) =>
|
|
174
|
+
Effect.logWarning(`Invalid profile schema in ${profilesPath}: ${e}`).pipe(
|
|
175
|
+
Effect.map(() => null)
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Load all auth profiles for a tool.
|
|
183
|
+
*
|
|
184
|
+
* Falls back to the legacy single-token file when `profiles.json` does not exist.
|
|
185
|
+
*
|
|
186
|
+
* @category Profile Storage
|
|
187
|
+
*/
|
|
188
|
+
export const loadProfiles = (
|
|
189
|
+
toolName: string
|
|
190
|
+
): Effect.Effect<
|
|
191
|
+
AuthProfilesFile,
|
|
192
|
+
FileSystemError | HomeDirectoryError,
|
|
193
|
+
FileSystem.FileSystem | Path.Path | HomeDirectoryTag
|
|
194
|
+
> =>
|
|
195
|
+
Effect.gen(function*() {
|
|
196
|
+
const profiles = yield* readProfilesFile(toolName)
|
|
197
|
+
if (profiles !== null) return profiles
|
|
198
|
+
const legacyToken = yield* loadToken(toolName)
|
|
199
|
+
const nowMs = yield* Clock.currentTimeMillis
|
|
200
|
+
return storeFromLegacyToken(legacyToken, new Date(nowMs))
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Save auth profiles and mirror the active profile to `auth.json`.
|
|
205
|
+
*
|
|
206
|
+
* @category Profile Storage
|
|
207
|
+
*/
|
|
208
|
+
export const saveProfiles = (
|
|
209
|
+
toolName: string,
|
|
210
|
+
store: AuthProfilesFile
|
|
211
|
+
) =>
|
|
212
|
+
Effect.gen(function*() {
|
|
213
|
+
const fs = yield* FileSystem.FileSystem
|
|
214
|
+
yield* ensureConfigDir(toolName)
|
|
215
|
+
const profilesPath = yield* getProfilesPath(toolName)
|
|
216
|
+
const active = activeProfileFromStore(store)
|
|
217
|
+
const normalized: AuthProfilesFile = {
|
|
218
|
+
activeProfileId: active?.id,
|
|
219
|
+
profiles: store.profiles
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
yield* writeSecureFile(profilesPath, JSON.stringify(normalized, null, 2)).pipe(
|
|
223
|
+
Effect.provideService(FileSystem.FileSystem, fs)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if (active) {
|
|
227
|
+
yield* saveToken(toolName, active.token)
|
|
228
|
+
} else {
|
|
229
|
+
yield* deleteToken(toolName)
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Insert or update a profile from a token and make it active.
|
|
235
|
+
*
|
|
236
|
+
* @category Profile Storage
|
|
237
|
+
*/
|
|
238
|
+
export const saveProfileToken = (
|
|
239
|
+
toolName: string,
|
|
240
|
+
token: OAuthToken
|
|
241
|
+
) =>
|
|
242
|
+
Effect.gen(function*() {
|
|
243
|
+
const store = yield* loadProfiles(toolName)
|
|
244
|
+
const nowMs = yield* Clock.currentTimeMillis
|
|
245
|
+
const next = profileFromToken(token, new Date(nowMs))
|
|
246
|
+
const existing = store.profiles.find((profile) => profile.id === next.id)
|
|
247
|
+
const profile: AuthProfile = existing
|
|
248
|
+
? { ...next, created_at: existing.created_at }
|
|
249
|
+
: next
|
|
250
|
+
const profiles = [
|
|
251
|
+
profile,
|
|
252
|
+
...store.profiles.filter((stored) => stored.id !== profile.id)
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
yield* saveProfiles(toolName, { activeProfileId: profile.id, profiles })
|
|
256
|
+
return profile
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Load the active auth profile.
|
|
261
|
+
*
|
|
262
|
+
* @category Profile Storage
|
|
263
|
+
*/
|
|
264
|
+
export const loadActiveProfile = (
|
|
265
|
+
toolName: string
|
|
266
|
+
) =>
|
|
267
|
+
loadProfiles(toolName).pipe(
|
|
268
|
+
Effect.map(activeProfileFromStore)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Load the active OAuth token.
|
|
273
|
+
*
|
|
274
|
+
* @category Profile Storage
|
|
275
|
+
*/
|
|
276
|
+
export const loadActiveProfileToken = (
|
|
277
|
+
toolName: string
|
|
278
|
+
) =>
|
|
279
|
+
loadActiveProfile(toolName).pipe(
|
|
280
|
+
Effect.map((profile) => profile?.token ?? null)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Set the active profile by ID.
|
|
285
|
+
*
|
|
286
|
+
* @category Profile Storage
|
|
287
|
+
*/
|
|
288
|
+
export const setActiveProfile = (
|
|
289
|
+
toolName: string,
|
|
290
|
+
profileId: string
|
|
291
|
+
) =>
|
|
292
|
+
Effect.gen(function*() {
|
|
293
|
+
const store = yield* loadProfiles(toolName)
|
|
294
|
+
const profile = store.profiles.find((stored) => stored.id === profileId)
|
|
295
|
+
if (!profile) return null
|
|
296
|
+
yield* saveProfiles(toolName, { ...store, activeProfileId: profile.id })
|
|
297
|
+
return profile
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Resolve a profile by ID, name, site URL, cloud ID, or account ID.
|
|
302
|
+
*
|
|
303
|
+
* @category Utilities
|
|
304
|
+
*/
|
|
305
|
+
export const findProfile = (
|
|
306
|
+
profiles: ReadonlyArray<AuthProfile>,
|
|
307
|
+
selector: string
|
|
308
|
+
): AuthProfile | null =>
|
|
309
|
+
profiles.find((profile) =>
|
|
310
|
+
profile.id === selector ||
|
|
311
|
+
profile.name === selector ||
|
|
312
|
+
profile.token.site_url === selector ||
|
|
313
|
+
profile.token.cloud_id === selector ||
|
|
314
|
+
profile.token.user?.account_id === selector
|
|
315
|
+
) ?? null
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Set the active profile using a flexible selector.
|
|
319
|
+
*
|
|
320
|
+
* @category Profile Storage
|
|
321
|
+
*/
|
|
322
|
+
export const setActiveProfileBySelector = (
|
|
323
|
+
toolName: string,
|
|
324
|
+
selector: string
|
|
325
|
+
) =>
|
|
326
|
+
Effect.gen(function*() {
|
|
327
|
+
const store = yield* loadProfiles(toolName)
|
|
328
|
+
const profile = findProfile(store.profiles, selector)
|
|
329
|
+
if (!profile) return null
|
|
330
|
+
yield* saveProfiles(toolName, { ...store, activeProfileId: profile.id })
|
|
331
|
+
return profile
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Remove a profile by ID.
|
|
336
|
+
*
|
|
337
|
+
* @category Profile Storage
|
|
338
|
+
*/
|
|
339
|
+
export const deleteProfile = (
|
|
340
|
+
toolName: string,
|
|
341
|
+
profileId: string
|
|
342
|
+
) =>
|
|
343
|
+
Effect.gen(function*() {
|
|
344
|
+
const store = yield* loadProfiles(toolName)
|
|
345
|
+
const profile = store.profiles.find((stored) => stored.id === profileId)
|
|
346
|
+
if (!profile) return null
|
|
347
|
+
const profiles = store.profiles.filter((stored) => stored.id !== profile.id)
|
|
348
|
+
const activeProfileId = store.activeProfileId === profile.id ? profiles[0]?.id : store.activeProfileId
|
|
349
|
+
yield* saveProfiles(toolName, { activeProfileId, profiles })
|
|
350
|
+
return profile
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Remove a profile using a flexible selector.
|
|
355
|
+
*
|
|
356
|
+
* @category Profile Storage
|
|
357
|
+
*/
|
|
358
|
+
export const deleteProfileBySelector = (
|
|
359
|
+
toolName: string,
|
|
360
|
+
selector: string
|
|
361
|
+
) =>
|
|
362
|
+
Effect.gen(function*() {
|
|
363
|
+
const store = yield* loadProfiles(toolName)
|
|
364
|
+
const profile = findProfile(store.profiles, selector)
|
|
365
|
+
if (!profile) return null
|
|
366
|
+
return yield* deleteProfile(toolName, profile.id)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Remove the active profile.
|
|
371
|
+
*
|
|
372
|
+
* @category Profile Storage
|
|
373
|
+
*/
|
|
374
|
+
export const deleteActiveProfile = (
|
|
375
|
+
toolName: string
|
|
376
|
+
) =>
|
|
377
|
+
Effect.gen(function*() {
|
|
378
|
+
const active = yield* loadActiveProfile(toolName)
|
|
379
|
+
if (!active) {
|
|
380
|
+
yield* deleteToken(toolName)
|
|
381
|
+
return null
|
|
382
|
+
}
|
|
383
|
+
return yield* deleteProfile(toolName, active.id)
|
|
384
|
+
})
|
|
@@ -136,6 +136,20 @@ export const getOAuthConfigPath = (
|
|
|
136
136
|
return path.join(configDir, "oauth.json")
|
|
137
137
|
})
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Get auth profiles file path for a specific tool.
|
|
141
|
+
*
|
|
142
|
+
* @category Utilities
|
|
143
|
+
*/
|
|
144
|
+
export const getProfilesPath = (
|
|
145
|
+
toolName: string
|
|
146
|
+
): Effect.Effect<string, HomeDirectoryError, HomeDirectoryTag | Path.Path> =>
|
|
147
|
+
Effect.gen(function*() {
|
|
148
|
+
const path = yield* Path.Path
|
|
149
|
+
const configDir = yield* getConfigDir(toolName)
|
|
150
|
+
return path.join(configDir, "profiles.json")
|
|
151
|
+
})
|
|
152
|
+
|
|
139
153
|
/**
|
|
140
154
|
* Ensure config directory exists with secure permissions.
|
|
141
155
|
*
|