@knpkv/atlassian-common 1.1.0 → 1.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 +25 -0
- package/README.md +6 -0
- package/dist/auth/OAuthEndpoints.d.ts +12 -0
- package/dist/auth/OAuthEndpoints.d.ts.map +1 -1
- package/dist/auth/OAuthEndpoints.js +16 -1
- package/dist/auth/OAuthEndpoints.js.map +1 -1
- package/dist/auth/OAuthOperations.d.ts +10 -0
- package/dist/auth/OAuthOperations.d.ts.map +1 -1
- package/dist/auth/OAuthOperations.js +12 -3
- package/dist/auth/OAuthOperations.js.map +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +5 -3
- package/dist/auth/index.js.map +1 -1
- package/dist/bin.js +2 -2
- package/dist/bin.js.map +1 -1
- package/dist/config/ProfileManager.d.ts +2 -0
- package/dist/config/ProfileManager.d.ts.map +1 -1
- package/dist/config/ProfileManager.js +16 -4
- package/dist/config/ProfileManager.js.map +1 -1
- package/dist/config/ProfileStorage.d.ts +5 -0
- package/dist/config/ProfileStorage.d.ts.map +1 -0
- package/dist/config/ProfileStorage.js +5 -0
- package/dist/config/ProfileStorage.js.map +1 -0
- package/dist/config/TokenStorage.d.ts +8 -0
- package/dist/config/TokenStorage.d.ts.map +1 -1
- package/dist/config/TokenStorage.js +15 -6
- package/dist/config/TokenStorage.js.map +1 -1
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -2
- package/dist/config/index.js.map +1 -1
- package/dist/internal/legacyWallClock.d.ts +11 -0
- package/dist/internal/legacyWallClock.d.ts.map +1 -0
- package/dist/internal/legacyWallClock.js +11 -0
- package/dist/internal/legacyWallClock.js.map +1 -0
- package/package.json +11 -7
- package/src/auth/OAuthEndpoints.ts +19 -1
- package/src/auth/OAuthOperations.ts +21 -4
- package/src/auth/index.ts +5 -1
- package/src/bin.ts +1 -4
- package/src/config/ProfileManager.ts +17 -4
- package/src/config/ProfileStorage.ts +5 -0
- package/src/config/TokenStorage.ts +20 -6
- package/src/config/index.ts +2 -0
- package/src/internal/legacyWallClock.ts +10 -0
- package/test/OAuthEndpoints.test.ts +14 -0
- package/test/OAuthOperations.test.ts +16 -0
- package/test/PublicExports.test.ts +16 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.tsbuildinfo +0 -1
package/dist/config/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { ensureConfigDir, getAuthPath, getConfigDir, getOAuthConfigPath, getProfilesPath, type HomeDirectory, HomeDirectoryError, HomeDirectoryLive, HomeDirectoryTag, writeSecureFile } from "./ConfigPaths.js";
|
|
7
7
|
export { type AtlassianSite, AtlassianSiteSchema, type AtlassianUser, AtlassianUserSchema, type OAuthConfig, OAuthConfigSchema, type OAuthToken, OAuthTokenSchema, type OAuthUser, OAuthUserSchema } from "./OAuthSchemas.js";
|
|
8
|
-
export { deleteToken, FileSystemError, isTokenExpired, loadOAuthConfig, loadToken, saveOAuthConfig, saveToken } from "./TokenStorage.js";
|
|
8
|
+
export { deleteToken, FileSystemError, isTokenExpired, isTokenExpiredAt, loadOAuthConfig, loadToken, saveOAuthConfig, saveToken } from "./TokenStorage.js";
|
|
9
9
|
export { type AuthProfile, AuthProfileSchema, type AuthProfilesFile, AuthProfilesFileSchema, deleteActiveProfile, deleteProfile, deleteProfileBySelector, findProfile, loadActiveProfile, loadActiveProfileToken, loadProfiles, profileFromToken, profileIdFromToken, profileNameFromToken, saveProfiles, saveProfileToken, setActiveProfile, setActiveProfileBySelector } from "./AuthProfiles.js";
|
|
10
|
-
export { ATLASSIAN_TOOLS, type AtlassianToolDefinition, CONFLUENCE_REQUIRED_SCOPES, inspectAllToolProfiles, inspectToolProfiles, JIRA_REQUIRED_SCOPES, migrateLegacyProfiles, MissingOAuthConfigError, missingScopes, ProfileNotFoundError, type ProfileTokenStatus, refreshActiveProfiles, tokenScopes, type ToolProfileStatus, useProfileForAllTools } from "./ProfileManager.js";
|
|
10
|
+
export { ATLASSIAN_TOOLS, type AtlassianToolDefinition, CONFLUENCE_REQUIRED_SCOPES, inspectAllToolProfiles, inspectToolProfiles, JIRA_PROPOSAL_REQUIRED_SCOPES, JIRA_REQUIRED_SCOPES, migrateLegacyProfiles, MissingOAuthConfigError, missingScopes, ProfileNotFoundError, type ProfileTokenStatus, refreshActiveProfiles, tokenScopes, type ToolProfileStatus, useProfileForAllTools } from "./ProfileManager.js";
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,eAAe,EACf,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,KAAK,aAAa,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EACL,KAAK,aAAa,EAClB,mBAAmB,EACnB,KAAK,aAAa,EAClB,mBAAmB,EACnB,KAAK,WAAW,EAChB,iBAAiB,EACjB,KAAK,UAAU,EACf,gBAAgB,EAChB,KAAK,SAAS,EACd,eAAe,EAChB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EACL,WAAW,EACX,eAAe,EACf,cAAc,EACd,eAAe,EACf,SAAS,EACT,eAAe,EACf,SAAS,EACV,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EACL,KAAK,WAAW,EAChB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,uBAAuB,EACvB,WAAW,EACX,iBAAiB,EACjB,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,0BAA0B,EAC3B,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EACL,eAAe,EACf,KAAK,uBAAuB,EAC5B,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,EACpB,KAAK,kBAAkB,EACvB,qBAAqB,EACrB,WAAW,EACX,KAAK,iBAAiB,EACtB,qBAAqB,EACtB,MAAM,qBAAqB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,eAAe,EACf,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,KAAK,aAAa,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EACL,KAAK,aAAa,EAClB,mBAAmB,EACnB,KAAK,aAAa,EAClB,mBAAmB,EACnB,KAAK,WAAW,EAChB,iBAAiB,EACjB,KAAK,UAAU,EACf,gBAAgB,EAChB,KAAK,SAAS,EACd,eAAe,EAChB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EACL,WAAW,EACX,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,eAAe,EACf,SAAS,EACV,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EACL,KAAK,WAAW,EAChB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,uBAAuB,EACvB,WAAW,EACX,iBAAiB,EACjB,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,0BAA0B,EAC3B,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EACL,eAAe,EACf,KAAK,uBAAuB,EAC5B,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,6BAA6B,EAC7B,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,EACpB,KAAK,kBAAkB,EACvB,qBAAqB,EACrB,WAAW,EACX,KAAK,iBAAiB,EACtB,qBAAqB,EACtB,MAAM,qBAAqB,CAAA"}
|
package/dist/config/index.js
CHANGED
|
@@ -8,9 +8,9 @@ export { ensureConfigDir, getAuthPath, getConfigDir, getOAuthConfigPath, getProf
|
|
|
8
8
|
// OAuth schemas
|
|
9
9
|
export { AtlassianSiteSchema, AtlassianUserSchema, OAuthConfigSchema, OAuthTokenSchema, OAuthUserSchema } from "./OAuthSchemas.js";
|
|
10
10
|
// Token storage
|
|
11
|
-
export { deleteToken, FileSystemError, isTokenExpired, loadOAuthConfig, loadToken, saveOAuthConfig, saveToken } from "./TokenStorage.js";
|
|
11
|
+
export { deleteToken, FileSystemError, isTokenExpired, isTokenExpiredAt, loadOAuthConfig, loadToken, saveOAuthConfig, saveToken } from "./TokenStorage.js";
|
|
12
12
|
// Auth profiles
|
|
13
13
|
export { AuthProfileSchema, AuthProfilesFileSchema, deleteActiveProfile, deleteProfile, deleteProfileBySelector, findProfile, loadActiveProfile, loadActiveProfileToken, loadProfiles, profileFromToken, profileIdFromToken, profileNameFromToken, saveProfiles, saveProfileToken, setActiveProfile, setActiveProfileBySelector } from "./AuthProfiles.js";
|
|
14
14
|
// Unified profile manager
|
|
15
|
-
export { ATLASSIAN_TOOLS, CONFLUENCE_REQUIRED_SCOPES, inspectAllToolProfiles, inspectToolProfiles, JIRA_REQUIRED_SCOPES, migrateLegacyProfiles, MissingOAuthConfigError, missingScopes, ProfileNotFoundError, refreshActiveProfiles, tokenScopes, useProfileForAllTools } from "./ProfileManager.js";
|
|
15
|
+
export { ATLASSIAN_TOOLS, CONFLUENCE_REQUIRED_SCOPES, inspectAllToolProfiles, inspectToolProfiles, JIRA_PROPOSAL_REQUIRED_SCOPES, JIRA_REQUIRED_SCOPES, migrateLegacyProfiles, MissingOAuthConfigError, missingScopes, ProfileNotFoundError, refreshActiveProfiles, tokenScopes, useProfileForAllTools } from "./ProfileManager.js";
|
|
16
16
|
//# sourceMappingURL=index.js.map
|
package/dist/config/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAe;AACf,OAAO,EACL,eAAe,EACf,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,eAAe,EAEf,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EAChB,MAAM,kBAAkB,CAAA;AAEzB,gBAAgB;AAChB,OAAO,EAEL,mBAAmB,EAEnB,mBAAmB,EAEnB,iBAAiB,EAEjB,gBAAgB,EAEhB,eAAe,EAChB,MAAM,mBAAmB,CAAA;AAE1B,gBAAgB;AAChB,OAAO,EACL,WAAW,EACX,eAAe,EACf,cAAc,EACd,eAAe,EACf,SAAS,EACT,eAAe,EACf,SAAS,EACV,MAAM,mBAAmB,CAAA;AAE1B,gBAAgB;AAChB,OAAO,EAEL,iBAAiB,EAEjB,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,uBAAuB,EACvB,WAAW,EACX,iBAAiB,EACjB,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,0BAA0B,EAC3B,MAAM,mBAAmB,CAAA;AAE1B,0BAA0B;AAC1B,OAAO,EACL,eAAe,EAEf,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,EAEpB,qBAAqB,EACrB,WAAW,EAEX,qBAAqB,EACtB,MAAM,qBAAqB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAe;AACf,OAAO,EACL,eAAe,EACf,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,eAAe,EAEf,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EAChB,MAAM,kBAAkB,CAAA;AAEzB,gBAAgB;AAChB,OAAO,EAEL,mBAAmB,EAEnB,mBAAmB,EAEnB,iBAAiB,EAEjB,gBAAgB,EAEhB,eAAe,EAChB,MAAM,mBAAmB,CAAA;AAE1B,gBAAgB;AAChB,OAAO,EACL,WAAW,EACX,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,eAAe,EACf,SAAS,EACV,MAAM,mBAAmB,CAAA;AAE1B,gBAAgB;AAChB,OAAO,EAEL,iBAAiB,EAEjB,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,uBAAuB,EACvB,WAAW,EACX,iBAAiB,EACjB,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,0BAA0B,EAC3B,MAAM,mBAAmB,CAAA;AAE1B,0BAA0B;AAC1B,OAAO,EACL,eAAe,EAEf,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,6BAA6B,EAC7B,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,EAEpB,qBAAqB,EACrB,WAAW,EAEX,qBAAqB,EACtB,MAAM,qBAAqB,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility boundary for legacy synchronous OAuth helpers.
|
|
3
|
+
*
|
|
4
|
+
* New Effect workflows must obtain time from `Clock.currentTimeMillis` and call
|
|
5
|
+
* the corresponding `*At` helper. This host-clock read remains only so the
|
|
6
|
+
* existing synchronous public API does not change without a major release.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export declare const unsafeCurrentTimeMillis: () => number;
|
|
11
|
+
//# sourceMappingURL=legacyWallClock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legacyWallClock.d.ts","sourceRoot":"","sources":["../../src/internal/legacyWallClock.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,QAAO,MAAoB,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility boundary for legacy synchronous OAuth helpers.
|
|
3
|
+
*
|
|
4
|
+
* New Effect workflows must obtain time from `Clock.currentTimeMillis` and call
|
|
5
|
+
* the corresponding `*At` helper. This host-clock read remains only so the
|
|
6
|
+
* existing synchronous public API does not change without a major release.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export const unsafeCurrentTimeMillis = () => Date.now();
|
|
11
|
+
//# sourceMappingURL=legacyWallClock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legacyWallClock.js","sourceRoot":"","sources":["../../src/internal/legacyWallClock.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knpkv/atlassian-common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Shared utilities for Atlassian tools (AST, serializers, auth, config)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "knpkv",
|
|
@@ -49,28 +49,32 @@
|
|
|
49
49
|
"./profile-manager": {
|
|
50
50
|
"import": "./dist/config/ProfileManager.js",
|
|
51
51
|
"types": "./dist/config/ProfileManager.d.ts"
|
|
52
|
+
},
|
|
53
|
+
"./profile-storage": {
|
|
54
|
+
"import": "./dist/config/ProfileStorage.js",
|
|
55
|
+
"types": "./dist/config/ProfileStorage.d.ts"
|
|
52
56
|
}
|
|
53
57
|
},
|
|
54
58
|
"publishConfig": {
|
|
55
59
|
"access": "public"
|
|
56
60
|
},
|
|
57
61
|
"peerDependencies": {
|
|
58
|
-
"effect": "4.0.0-beta.
|
|
62
|
+
"effect": "4.0.0-beta.98"
|
|
59
63
|
},
|
|
60
64
|
"dependencies": {
|
|
61
|
-
"@effect/platform-node": "4.0.0-beta.
|
|
65
|
+
"@effect/platform-node": "4.0.0-beta.98",
|
|
62
66
|
"remark-gfm": "^4.0.1",
|
|
63
67
|
"remark-parse": "^11.0.0",
|
|
64
68
|
"remark-stringify": "^11.0.0",
|
|
65
69
|
"unified": "^11.0.5"
|
|
66
70
|
},
|
|
67
71
|
"devDependencies": {
|
|
68
|
-
"@effect/platform-node": "4.0.0-beta.
|
|
69
|
-
"@effect/vitest": "4.0.0-beta.
|
|
72
|
+
"@effect/platform-node": "4.0.0-beta.98",
|
|
73
|
+
"@effect/vitest": "4.0.0-beta.98",
|
|
70
74
|
"@types/node": "latest",
|
|
71
|
-
"effect": "4.0.0-beta.
|
|
75
|
+
"effect": "4.0.0-beta.98",
|
|
72
76
|
"typescript": "~6.0.3",
|
|
73
|
-
"vitest": "^4.1.
|
|
77
|
+
"vitest": "^4.1.10"
|
|
74
78
|
},
|
|
75
79
|
"keywords": [
|
|
76
80
|
"effect",
|
|
@@ -82,6 +82,22 @@ export const JIRA_SCOPES = [
|
|
|
82
82
|
"offline_access"
|
|
83
83
|
]
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Legacy proposal-only Jira scopes retained for public API compatibility.
|
|
87
|
+
*
|
|
88
|
+
* Control Center owns a separate least-privilege release-publication scope set;
|
|
89
|
+
* {@link JIRA_SCOPES} remains the legacy write-capable set for other consumers,
|
|
90
|
+
* while proposal-only integrations may continue using this read-only set.
|
|
91
|
+
*
|
|
92
|
+
* @category Scopes
|
|
93
|
+
*/
|
|
94
|
+
export const JIRA_PROPOSAL_SCOPES = [
|
|
95
|
+
"read:jira-work",
|
|
96
|
+
"read:jira-user",
|
|
97
|
+
"read:me",
|
|
98
|
+
"offline_access"
|
|
99
|
+
]
|
|
100
|
+
|
|
85
101
|
/**
|
|
86
102
|
* Options for building OAuth2 authorization URL.
|
|
87
103
|
*
|
|
@@ -94,6 +110,8 @@ export interface BuildAuthUrlOptions {
|
|
|
94
110
|
readonly state: string
|
|
95
111
|
/** Local callback server port */
|
|
96
112
|
readonly port: number
|
|
113
|
+
/** Exact callback URL for an existing application server. */
|
|
114
|
+
readonly redirectUri?: string | undefined
|
|
97
115
|
/** OAuth2 scopes */
|
|
98
116
|
readonly scopes: ReadonlyArray<string>
|
|
99
117
|
/** PKCE code_challenge (S256). Omit to skip PKCE. */
|
|
@@ -110,7 +128,7 @@ export const buildAuthUrl = (options: BuildAuthUrlOptions): string => {
|
|
|
110
128
|
audience: "api.atlassian.com",
|
|
111
129
|
client_id: options.clientId,
|
|
112
130
|
scope: options.scopes.join(" "),
|
|
113
|
-
redirect_uri: `http://localhost:${options.port}/callback`,
|
|
131
|
+
redirect_uri: options.redirectUri ?? `http://localhost:${options.port}/callback`,
|
|
114
132
|
state: options.state,
|
|
115
133
|
response_type: "code",
|
|
116
134
|
prompt: "consent"
|
|
@@ -23,6 +23,7 @@ import * as Effect from "effect/Effect"
|
|
|
23
23
|
import * as Schema from "effect/Schema"
|
|
24
24
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
25
25
|
import { type OAuthConfig, type OAuthToken } from "../config/OAuthSchemas.js"
|
|
26
|
+
import { unsafeCurrentTimeMillis } from "../internal/legacyWallClock.js"
|
|
26
27
|
import { ME_URL, RESOURCES_URL, REVOKE_URL, TOKEN_URL } from "./OAuthEndpoints.js"
|
|
27
28
|
import { OAuthError } from "./OAuthErrors.js"
|
|
28
29
|
import {
|
|
@@ -42,6 +43,8 @@ import {
|
|
|
42
43
|
export interface ExchangeCodeOptions {
|
|
43
44
|
/** Local callback server port (for redirect_uri) */
|
|
44
45
|
readonly port: number
|
|
46
|
+
/** Exact callback URL used by the authorization request. */
|
|
47
|
+
readonly redirectUri?: string | undefined
|
|
45
48
|
/** PKCE code verifier (if PKCE was used in auth request) */
|
|
46
49
|
readonly codeVerifier?: string | undefined
|
|
47
50
|
}
|
|
@@ -63,7 +66,7 @@ export const exchangeCodeForTokens = (
|
|
|
63
66
|
client_id: config.clientId,
|
|
64
67
|
client_secret: config.clientSecret,
|
|
65
68
|
code,
|
|
66
|
-
redirect_uri: `http://localhost:${options.port}/callback`
|
|
69
|
+
redirect_uri: options.redirectUri ?? `http://localhost:${options.port}/callback`
|
|
67
70
|
}
|
|
68
71
|
if (options.codeVerifier) tokenBody.code_verifier = options.codeVerifier
|
|
69
72
|
const request = yield* HttpClientRequest.post(TOKEN_URL).pipe(
|
|
@@ -258,14 +261,15 @@ export const revokeToken = (
|
|
|
258
261
|
*
|
|
259
262
|
* @category Utilities
|
|
260
263
|
*/
|
|
261
|
-
export const
|
|
264
|
+
export const buildOAuthTokenAt = (
|
|
262
265
|
tokenResponse: TokenResponse,
|
|
263
266
|
site: AccessibleResource,
|
|
264
|
-
user: UserInfo
|
|
267
|
+
user: UserInfo,
|
|
268
|
+
nowMs: number
|
|
265
269
|
): OAuthToken => ({
|
|
266
270
|
access_token: tokenResponse.access_token,
|
|
267
271
|
refresh_token: tokenResponse.refresh_token,
|
|
268
|
-
expires_at:
|
|
272
|
+
expires_at: nowMs + tokenResponse.expires_in * 1000,
|
|
269
273
|
scope: tokenResponse.scope,
|
|
270
274
|
cloud_id: site.id,
|
|
271
275
|
site_url: site.url,
|
|
@@ -275,3 +279,16 @@ export const buildOAuthToken = (
|
|
|
275
279
|
email: user.email
|
|
276
280
|
}
|
|
277
281
|
})
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Build an OAuth token using the host clock.
|
|
285
|
+
*
|
|
286
|
+
* @deprecated Effect workflows should call {@link buildOAuthTokenAt} with
|
|
287
|
+
* `Clock.currentTimeMillis`. This wrapper preserves the existing synchronous
|
|
288
|
+
* public API until the next major release.
|
|
289
|
+
*/
|
|
290
|
+
export const buildOAuthToken = (
|
|
291
|
+
tokenResponse: TokenResponse,
|
|
292
|
+
site: AccessibleResource,
|
|
293
|
+
user: UserInfo
|
|
294
|
+
): OAuthToken => buildOAuthTokenAt(tokenResponse, site, user, unsafeCurrentTimeMillis())
|
package/src/auth/index.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
// Endpoints and
|
|
7
|
+
// Endpoints and legacy public scope sets. Control Center owns a separate
|
|
8
|
+
// least-privilege release-publication set; proposal-only consumers can use
|
|
9
|
+
// JIRA_PROPOSAL_SCOPES and other write-capable consumers can use JIRA_SCOPES.
|
|
8
10
|
export {
|
|
9
11
|
AUTH_URL,
|
|
10
12
|
buildAuthUrl,
|
|
@@ -12,6 +14,7 @@ export {
|
|
|
12
14
|
computeCodeChallenge,
|
|
13
15
|
CONFLUENCE_SCOPES,
|
|
14
16
|
generateCodeVerifier,
|
|
17
|
+
JIRA_PROPOSAL_SCOPES,
|
|
15
18
|
JIRA_SCOPES,
|
|
16
19
|
ME_URL,
|
|
17
20
|
RESOURCES_URL,
|
|
@@ -25,6 +28,7 @@ export { AuthMissingError, OAuthError, OAuthNotConfiguredError, type OAuthStep }
|
|
|
25
28
|
// Operations
|
|
26
29
|
export {
|
|
27
30
|
buildOAuthToken,
|
|
31
|
+
buildOAuthTokenAt,
|
|
28
32
|
exchangeCodeForTokens,
|
|
29
33
|
type ExchangeCodeOptions,
|
|
30
34
|
getAccessibleResources,
|
package/src/bin.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { NodeHttpClient, NodeRuntime, NodeServices } from "@effect/platform-node"
|
|
3
3
|
import * as Console from "effect/Console"
|
|
4
4
|
import * as Effect from "effect/Effect"
|
|
5
5
|
import * as Layer from "effect/Layer"
|
|
@@ -99,12 +99,9 @@ const program = Effect.gen(function*() {
|
|
|
99
99
|
const args = yield* stdio.args
|
|
100
100
|
return yield* cli(args)
|
|
101
101
|
}).pipe(
|
|
102
|
-
Effect.provide(NodeStdio.layer),
|
|
103
102
|
Effect.provide(
|
|
104
103
|
Layer.mergeAll(
|
|
105
104
|
NodeServices.layer,
|
|
106
|
-
NodeFileSystem.layer,
|
|
107
|
-
NodePath.layer,
|
|
108
105
|
NodeHttpClient.layerFetch,
|
|
109
106
|
HomeDirectoryLive
|
|
110
107
|
)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
+
import * as Clock from "effect/Clock"
|
|
6
7
|
import * as Data from "effect/Data"
|
|
7
8
|
import * as Effect from "effect/Effect"
|
|
8
9
|
import * as FileSystem from "effect/FileSystem"
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
} from "./AuthProfiles.js"
|
|
23
24
|
import { getProfilesPath, type HomeDirectoryError, HomeDirectoryTag } from "./ConfigPaths.js"
|
|
24
25
|
import { type OAuthToken, OAuthTokenSchema } from "./OAuthSchemas.js"
|
|
25
|
-
import { FileSystemError,
|
|
26
|
+
import { FileSystemError, isTokenExpiredAt, loadOAuthConfig } from "./TokenStorage.js"
|
|
26
27
|
|
|
27
28
|
export interface AtlassianToolDefinition {
|
|
28
29
|
readonly toolName: string
|
|
@@ -42,6 +43,14 @@ export const JIRA_REQUIRED_SCOPES: ReadonlyArray<string> = [
|
|
|
42
43
|
"offline_access"
|
|
43
44
|
]
|
|
44
45
|
|
|
46
|
+
/** Legacy read-only Jira requirements retained for proposal-only consumers. */
|
|
47
|
+
export const JIRA_PROPOSAL_REQUIRED_SCOPES: ReadonlyArray<string> = [
|
|
48
|
+
"read:jira-work",
|
|
49
|
+
"read:jira-user",
|
|
50
|
+
"read:me",
|
|
51
|
+
"offline_access"
|
|
52
|
+
]
|
|
53
|
+
|
|
45
54
|
export const CONFLUENCE_REQUIRED_SCOPES: ReadonlyArray<string> = [
|
|
46
55
|
"read:page:confluence",
|
|
47
56
|
"write:page:confluence",
|
|
@@ -135,7 +144,7 @@ const loadLegacyToken = (
|
|
|
135
144
|
)
|
|
136
145
|
const parsed = yield* Effect.try({
|
|
137
146
|
try: () => JSON.parse(content),
|
|
138
|
-
catch: (
|
|
147
|
+
catch: (): "invalid-json" => "invalid-json"
|
|
139
148
|
}).pipe(Effect.catch(() => Effect.succeed(null)))
|
|
140
149
|
if (parsed === null) return null
|
|
141
150
|
return yield* Schema.decodeUnknownEffect(OAuthTokenSchema)(parsed).pipe(
|
|
@@ -166,12 +175,15 @@ export const inspectToolProfiles = (
|
|
|
166
175
|
const [store, config] = yield* Effect.all([loadProfiles(storeName), loadOAuthConfig(storeName)])
|
|
167
176
|
const activeProfile = store.profiles.find((profile) => profile.id === store.activeProfileId) ?? store.profiles[0] ??
|
|
168
177
|
null
|
|
178
|
+
const nowMs = yield* Clock.currentTimeMillis
|
|
169
179
|
return {
|
|
170
180
|
tool,
|
|
171
181
|
authStoreName: storeName,
|
|
172
182
|
activeProfile,
|
|
173
183
|
profiles: store.profiles,
|
|
174
|
-
tokenStatus: activeProfile === null ? "missing" :
|
|
184
|
+
tokenStatus: activeProfile === null ? "missing" : isTokenExpiredAt(activeProfile.token, nowMs, 0)
|
|
185
|
+
? "expired"
|
|
186
|
+
: "valid",
|
|
175
187
|
missingScopes: activeProfile === null ? [] : missingScopes(activeProfile.token, tool.requiredScopes),
|
|
176
188
|
oauthConfigured: config !== null
|
|
177
189
|
}
|
|
@@ -268,7 +280,8 @@ export const refreshActiveProfiles = (
|
|
|
268
280
|
const store = yield* loadProfiles(storeName)
|
|
269
281
|
const active = store.profiles.find((profile) => profile.id === store.activeProfileId) ?? store.profiles[0] ??
|
|
270
282
|
null
|
|
271
|
-
|
|
283
|
+
const nowMs = yield* Clock.currentTimeMillis
|
|
284
|
+
if (!active || !isTokenExpiredAt(active.token, nowMs, 0)) return
|
|
272
285
|
const config = yield* loadOAuthConfig(storeName)
|
|
273
286
|
if (!config) {
|
|
274
287
|
return yield* Effect.fail(new MissingOAuthConfigError({ authStoreName: storeName, profileId: active.id }))
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Filesystem-only Atlassian profile storage used by local service integrations. @module */
|
|
2
|
+
|
|
3
|
+
export { type AuthProfile, loadProfiles } from "./AuthProfiles.js"
|
|
4
|
+
export { HomeDirectoryError, HomeDirectoryLive, HomeDirectoryTag } from "./ConfigPaths.js"
|
|
5
|
+
export { FileSystemError, isTokenExpired } from "./TokenStorage.js"
|
|
@@ -23,6 +23,7 @@ import * as FileSystem from "effect/FileSystem"
|
|
|
23
23
|
import type * as Path from "effect/Path"
|
|
24
24
|
import type * as PlatformError from "effect/PlatformError"
|
|
25
25
|
import * as Schema from "effect/Schema"
|
|
26
|
+
import { unsafeCurrentTimeMillis } from "../internal/legacyWallClock.js"
|
|
26
27
|
import { type HomeDirectoryError, type HomeDirectoryTag } from "./ConfigPaths.js"
|
|
27
28
|
import { ensureConfigDir, getAuthPath, getOAuthConfigPath, writeSecureFile } from "./ConfigPaths.js"
|
|
28
29
|
import { type OAuthConfig, OAuthConfigSchema, type OAuthToken, OAuthTokenSchema } from "./OAuthSchemas.js"
|
|
@@ -70,7 +71,7 @@ export const loadToken = (
|
|
|
70
71
|
const tokenPath = yield* getAuthPath(toolName)
|
|
71
72
|
|
|
72
73
|
const exists = yield* fs.exists(tokenPath).pipe(
|
|
73
|
-
Effect.
|
|
74
|
+
Effect.catch(() => Effect.succeed(false))
|
|
74
75
|
)
|
|
75
76
|
if (!exists) {
|
|
76
77
|
return null
|
|
@@ -87,7 +88,7 @@ export const loadToken = (
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
return yield* Schema.decodeUnknownEffect(OAuthTokenSchema)(parsed).pipe(
|
|
90
|
-
Effect.
|
|
91
|
+
Effect.catch((e) =>
|
|
91
92
|
Effect.logWarning(`Invalid token schema in ${tokenPath}: ${e}`).pipe(
|
|
92
93
|
Effect.map(() => null)
|
|
93
94
|
)
|
|
@@ -136,7 +137,7 @@ export const deleteToken = (
|
|
|
136
137
|
const tokenPath = yield* getAuthPath(toolName)
|
|
137
138
|
|
|
138
139
|
yield* fs.remove(tokenPath).pipe(
|
|
139
|
-
Effect.
|
|
140
|
+
Effect.catch(() => Effect.void)
|
|
140
141
|
)
|
|
141
142
|
})
|
|
142
143
|
|
|
@@ -160,7 +161,7 @@ export const loadOAuthConfig = (
|
|
|
160
161
|
const configPath = yield* getOAuthConfigPath(toolName)
|
|
161
162
|
|
|
162
163
|
const exists = yield* fs.exists(configPath).pipe(
|
|
163
|
-
Effect.
|
|
164
|
+
Effect.catch(() => Effect.succeed(false))
|
|
164
165
|
)
|
|
165
166
|
if (!exists) {
|
|
166
167
|
return null
|
|
@@ -177,7 +178,7 @@ export const loadOAuthConfig = (
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
return yield* Schema.decodeUnknownEffect(OAuthConfigSchema)(parsed).pipe(
|
|
180
|
-
Effect.
|
|
181
|
+
Effect.catch((e) =>
|
|
181
182
|
Effect.logWarning(`Invalid OAuth config schema in ${configPath}: ${e}`).pipe(
|
|
182
183
|
Effect.map(() => null)
|
|
183
184
|
)
|
|
@@ -220,5 +221,18 @@ export const saveOAuthConfig = (
|
|
|
220
221
|
*
|
|
221
222
|
* @category Utilities
|
|
222
223
|
*/
|
|
224
|
+
export const isTokenExpiredAt = (
|
|
225
|
+
token: OAuthToken,
|
|
226
|
+
nowMs: number,
|
|
227
|
+
bufferMs: number = 5 * 60 * 1000
|
|
228
|
+
): boolean => nowMs >= token.expires_at - bufferMs
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Check token expiration using the host clock.
|
|
232
|
+
*
|
|
233
|
+
* @deprecated Effect workflows should call {@link isTokenExpiredAt} with
|
|
234
|
+
* `Clock.currentTimeMillis`. This wrapper preserves the existing synchronous
|
|
235
|
+
* public API until the next major release.
|
|
236
|
+
*/
|
|
223
237
|
export const isTokenExpired = (token: OAuthToken, bufferMs: number = 5 * 60 * 1000): boolean =>
|
|
224
|
-
|
|
238
|
+
isTokenExpiredAt(token, unsafeCurrentTimeMillis(), bufferMs)
|
package/src/config/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ export {
|
|
|
37
37
|
deleteToken,
|
|
38
38
|
FileSystemError,
|
|
39
39
|
isTokenExpired,
|
|
40
|
+
isTokenExpiredAt,
|
|
40
41
|
loadOAuthConfig,
|
|
41
42
|
loadToken,
|
|
42
43
|
saveOAuthConfig,
|
|
@@ -72,6 +73,7 @@ export {
|
|
|
72
73
|
CONFLUENCE_REQUIRED_SCOPES,
|
|
73
74
|
inspectAllToolProfiles,
|
|
74
75
|
inspectToolProfiles,
|
|
76
|
+
JIRA_PROPOSAL_REQUIRED_SCOPES,
|
|
75
77
|
JIRA_REQUIRED_SCOPES,
|
|
76
78
|
migrateLegacyProfiles,
|
|
77
79
|
MissingOAuthConfigError,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility boundary for legacy synchronous OAuth helpers.
|
|
3
|
+
*
|
|
4
|
+
* New Effect workflows must obtain time from `Clock.currentTimeMillis` and call
|
|
5
|
+
* the corresponding `*At` helper. This host-clock read remains only so the
|
|
6
|
+
* existing synchronous public API does not change without a major release.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export const unsafeCurrentTimeMillis = (): number => Date.now()
|
|
@@ -90,6 +90,20 @@ describe("OAuthEndpoints", () => {
|
|
|
90
90
|
expect(parsed.searchParams.get("prompt")).toBe("consent")
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
+
it("uses an application callback URL when supplied", () => {
|
|
94
|
+
const url = buildAuthUrl({
|
|
95
|
+
clientId: "cid",
|
|
96
|
+
state: "state-123",
|
|
97
|
+
port: 4173,
|
|
98
|
+
redirectUri: "http://127.0.0.1:4173/services/oauth/atlassian/callback",
|
|
99
|
+
scopes: ["read:me"]
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(new URL(url).searchParams.get("redirect_uri")).toBe(
|
|
103
|
+
"http://127.0.0.1:4173/services/oauth/atlassian/callback"
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
93
107
|
// PKCE is optional — omitting codeChallenge must not add params (backwards compat)
|
|
94
108
|
it("omits PKCE params when codeChallenge not provided", () => {
|
|
95
109
|
const url = buildAuthUrl({
|
|
@@ -127,6 +127,22 @@ describe("OAuthOperations", () => {
|
|
|
127
127
|
expect(body.code_verifier).toBe("my-verifier")
|
|
128
128
|
}))
|
|
129
129
|
|
|
130
|
+
it.effect("uses the same application callback URL during exchange", () =>
|
|
131
|
+
Effect.gen(function*() {
|
|
132
|
+
const { capturedRequests, mockClient } = createMockHttpClient([{ status: 200, body: validTokenBody }])
|
|
133
|
+
const redirectUri = "http://127.0.0.1:4173/services/oauth/atlassian/callback"
|
|
134
|
+
|
|
135
|
+
yield* exchangeCodeForTokens("auth-code", oauthConfig, { port: 4173, redirectUri }).pipe(
|
|
136
|
+
provide(mockClient)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const request = capturedRequests[0]!
|
|
140
|
+
const body = yield* request.body._tag === "Uint8Array"
|
|
141
|
+
? Effect.succeed(JSON.parse(new TextDecoder().decode(request.body.body)))
|
|
142
|
+
: Effect.succeed({})
|
|
143
|
+
expect(body.redirect_uri).toBe(redirectUri)
|
|
144
|
+
}))
|
|
145
|
+
|
|
130
146
|
// Token exchange failures (invalid code, expired code) must surface as OAuthError with step="token"
|
|
131
147
|
it.effect("returns OAuthError with step=token on HTTP 400", () =>
|
|
132
148
|
Effect.gen(function*() {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { JIRA_PROPOSAL_SCOPES, JIRA_SCOPES } from "../src/auth/index.js"
|
|
2
|
+
import { JIRA_PROPOSAL_REQUIRED_SCOPES, JIRA_REQUIRED_SCOPES } from "../src/config/index.js"
|
|
3
|
+
|
|
4
|
+
describe("public compatibility exports", () => {
|
|
5
|
+
it("retains proposal-only Jira scopes beside the write-capable release scopes", () => {
|
|
6
|
+
expect(JIRA_PROPOSAL_SCOPES).toEqual([
|
|
7
|
+
"read:jira-work",
|
|
8
|
+
"read:jira-user",
|
|
9
|
+
"read:me",
|
|
10
|
+
"offline_access"
|
|
11
|
+
])
|
|
12
|
+
expect(JIRA_PROPOSAL_REQUIRED_SCOPES).toEqual(JIRA_PROPOSAL_SCOPES)
|
|
13
|
+
expect(JIRA_SCOPES).toContain("manage:jira-configuration")
|
|
14
|
+
expect(JIRA_REQUIRED_SCOPES).toContain("manage:jira-project")
|
|
15
|
+
})
|
|
16
|
+
})
|