@chronicle.app/auth 0.1.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/LICENSE +21 -0
- package/README.md +4 -0
- package/dist/CredentialManager.d.ts +49 -0
- package/dist/CredentialManager.js +69 -0
- package/dist/FileCredentialManager.d.ts +78 -0
- package/dist/FileCredentialManager.js +241 -0
- package/dist/OAuthProvider.d.ts +57 -0
- package/dist/OAuthProvider.js +129 -0
- package/dist/OAuthServer.d.ts +16 -0
- package/dist/OAuthServer.js +89 -0
- package/dist/ProviderRegistry.d.ts +25 -0
- package/dist/ProviderRegistry.js +36 -0
- package/dist/TokenHelper.d.ts +23 -0
- package/dist/TokenHelper.js +56 -0
- package/dist/configDir.d.ts +10 -0
- package/dist/configDir.js +16 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/resolveCredentials.d.ts +36 -0
- package/dist/resolveCredentials.js +44 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Louis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { TokenResponse } from './types.js';
|
|
2
|
+
export interface StoredCredentials {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken?: string;
|
|
5
|
+
tokenType: string;
|
|
6
|
+
expiresIn?: number;
|
|
7
|
+
scope?: string;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
clientId?: string;
|
|
10
|
+
clientSecret?: string;
|
|
11
|
+
username?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class CredentialManager {
|
|
14
|
+
private static fileManager;
|
|
15
|
+
private static getFileManager;
|
|
16
|
+
/**
|
|
17
|
+
* Store OAuth credentials in local file
|
|
18
|
+
*/
|
|
19
|
+
static storeCredentials(provider: string, credentials: TokenResponse, clientId?: string, clientSecret?: string): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Retrieve stored credentials from file
|
|
22
|
+
*/
|
|
23
|
+
static getCredentials(provider: string): Promise<StoredCredentials | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Remove credentials from file
|
|
26
|
+
*/
|
|
27
|
+
static removeCredentials(provider: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Check if stored access token is expired
|
|
30
|
+
*/
|
|
31
|
+
static isTokenExpired(credentials: StoredCredentials): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Get valid access token, checking expiration
|
|
34
|
+
*/
|
|
35
|
+
static getValidToken(provider: string): Promise<string | null>;
|
|
36
|
+
/**
|
|
37
|
+
* List all stored provider credentials
|
|
38
|
+
*/
|
|
39
|
+
static listProviders(): Promise<string[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Check if credentials exist for a provider
|
|
42
|
+
*/
|
|
43
|
+
static hasCredentials(provider: string): Promise<boolean>;
|
|
44
|
+
/**
|
|
45
|
+
* Get the path where credentials are stored (for user reference)
|
|
46
|
+
*/
|
|
47
|
+
static getCredentialsPath(): string;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=CredentialManager.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { chronicleConfigDir } from './configDir.js';
|
|
2
|
+
import { FileCredentialManager } from './FileCredentialManager.js';
|
|
3
|
+
export class CredentialManager {
|
|
4
|
+
static fileManager = null;
|
|
5
|
+
static getFileManager() {
|
|
6
|
+
if (!this.fileManager) {
|
|
7
|
+
// The same config directory oclif resolves, so the file matches what
|
|
8
|
+
// `chronicle auth set` writes from inside a command.
|
|
9
|
+
this.fileManager = new FileCredentialManager(chronicleConfigDir());
|
|
10
|
+
}
|
|
11
|
+
return this.fileManager;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Store OAuth credentials in local file
|
|
15
|
+
*/
|
|
16
|
+
static async storeCredentials(provider, credentials, clientId, clientSecret) {
|
|
17
|
+
const fileManager = this.getFileManager();
|
|
18
|
+
await fileManager.storeCredentials(provider, credentials, clientId, clientSecret);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Retrieve stored credentials from file
|
|
22
|
+
*/
|
|
23
|
+
static async getCredentials(provider) {
|
|
24
|
+
const fileManager = this.getFileManager();
|
|
25
|
+
return fileManager.getCredentials(provider);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Remove credentials from file
|
|
29
|
+
*/
|
|
30
|
+
static async removeCredentials(provider) {
|
|
31
|
+
const fileManager = this.getFileManager();
|
|
32
|
+
await fileManager.removeCredentials(provider);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if stored access token is expired
|
|
36
|
+
*/
|
|
37
|
+
static isTokenExpired(credentials) {
|
|
38
|
+
const fileManager = this.getFileManager();
|
|
39
|
+
return fileManager.isTokenExpired(credentials);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get valid access token, checking expiration
|
|
43
|
+
*/
|
|
44
|
+
static async getValidToken(provider) {
|
|
45
|
+
const fileManager = this.getFileManager();
|
|
46
|
+
return fileManager.getValidToken(provider);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* List all stored provider credentials
|
|
50
|
+
*/
|
|
51
|
+
static async listProviders() {
|
|
52
|
+
const fileManager = this.getFileManager();
|
|
53
|
+
return fileManager.listProviders();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check if credentials exist for a provider
|
|
57
|
+
*/
|
|
58
|
+
static async hasCredentials(provider) {
|
|
59
|
+
const fileManager = this.getFileManager();
|
|
60
|
+
return fileManager.hasCredentials(provider);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the path where credentials are stored (for user reference)
|
|
64
|
+
*/
|
|
65
|
+
static getCredentialsPath() {
|
|
66
|
+
const fileManager = this.getFileManager();
|
|
67
|
+
return fileManager.getCredentialsPath();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { TokenResponse } from './types.js';
|
|
2
|
+
export interface StoredCredentials {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken?: string;
|
|
5
|
+
tokenType: string;
|
|
6
|
+
expiresIn?: number;
|
|
7
|
+
scope?: string;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
clientId?: string;
|
|
10
|
+
clientSecret?: string;
|
|
11
|
+
username?: string;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
export declare class FileCredentialManager {
|
|
15
|
+
private credentialsPath;
|
|
16
|
+
constructor(configDir: string);
|
|
17
|
+
/**
|
|
18
|
+
* Load all credentials from file
|
|
19
|
+
*/
|
|
20
|
+
private loadCredentials;
|
|
21
|
+
/**
|
|
22
|
+
* Migrate old credential format (single objects) to new format (arrays)
|
|
23
|
+
*/
|
|
24
|
+
private migrateCredentialsFormat;
|
|
25
|
+
/**
|
|
26
|
+
* Save all credentials to file
|
|
27
|
+
*/
|
|
28
|
+
private saveCredentials;
|
|
29
|
+
/**
|
|
30
|
+
* Store OAuth credentials for a provider
|
|
31
|
+
*/
|
|
32
|
+
storeCredentials(provider: string, tokenResponse: TokenResponse, clientId?: string, clientSecret?: string): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Update stored credentials directly (used for token refresh)
|
|
35
|
+
*/
|
|
36
|
+
private updateStoredCredentials;
|
|
37
|
+
/**
|
|
38
|
+
* Retrieve stored credentials for a provider (returns most recent credential)
|
|
39
|
+
*/
|
|
40
|
+
getCredentials(provider: string): Promise<StoredCredentials | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Retrieve all stored credentials for a provider
|
|
43
|
+
*/
|
|
44
|
+
getAllCredentials(provider: string): Promise<StoredCredentials[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Remove credentials for a provider
|
|
47
|
+
*/
|
|
48
|
+
removeCredentials(provider: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Check if stored access token is expired
|
|
51
|
+
*/
|
|
52
|
+
isTokenExpired(credentials: StoredCredentials): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Get valid access token, checking expiration
|
|
55
|
+
*/
|
|
56
|
+
getValidToken(provider: string): Promise<string | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Refresh an expired token using the refresh token
|
|
59
|
+
*/
|
|
60
|
+
private refreshToken;
|
|
61
|
+
/**
|
|
62
|
+
* Get the token refresh URL for a specific provider
|
|
63
|
+
*/
|
|
64
|
+
private getRefreshUrl;
|
|
65
|
+
/**
|
|
66
|
+
* List all providers with stored credentials
|
|
67
|
+
*/
|
|
68
|
+
listProviders(): Promise<string[]>;
|
|
69
|
+
/**
|
|
70
|
+
* Check if credentials exist for a provider
|
|
71
|
+
*/
|
|
72
|
+
hasCredentials(provider: string): Promise<boolean>;
|
|
73
|
+
/**
|
|
74
|
+
* Get the path where credentials are stored (for user reference)
|
|
75
|
+
*/
|
|
76
|
+
getCredentialsPath(): string;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=FileCredentialManager.d.ts.map
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
export class FileCredentialManager {
|
|
4
|
+
credentialsPath;
|
|
5
|
+
constructor(configDir) {
|
|
6
|
+
this.credentialsPath = join(configDir, 'credentials.json');
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Load all credentials from file
|
|
10
|
+
*/
|
|
11
|
+
async loadCredentials() {
|
|
12
|
+
try {
|
|
13
|
+
const credentialsText = await fs.readFile(this.credentialsPath, 'utf-8');
|
|
14
|
+
const parsed = JSON.parse(credentialsText);
|
|
15
|
+
// Migrate old format to new array format
|
|
16
|
+
return this.migrateCredentialsFormat(parsed);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT') {
|
|
20
|
+
// Credentials file doesn't exist, return empty
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
// If file is invalid, return empty and log warning
|
|
24
|
+
console.warn('Invalid credentials file, starting fresh:', error);
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Migrate old credential format (single objects) to new format (arrays)
|
|
30
|
+
*/
|
|
31
|
+
migrateCredentialsFormat(credentials) {
|
|
32
|
+
const migrated = {};
|
|
33
|
+
for (const [provider, creds] of Object.entries(credentials)) {
|
|
34
|
+
if (Array.isArray(creds)) {
|
|
35
|
+
// Already in new format
|
|
36
|
+
migrated[provider] = creds;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Old format - convert to array
|
|
40
|
+
migrated[provider] = [creds];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return migrated;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Save all credentials to file
|
|
47
|
+
*/
|
|
48
|
+
async saveCredentials(credentials) {
|
|
49
|
+
// Ensure config directory exists
|
|
50
|
+
await fs.mkdir(dirname(this.credentialsPath), { recursive: true });
|
|
51
|
+
// Write credentials file with proper formatting and permissions
|
|
52
|
+
await fs.writeFile(this.credentialsPath, JSON.stringify(credentials, null, 2), { mode: 0o600 } // Restrict to user read/write only
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Store OAuth credentials for a provider
|
|
57
|
+
*/
|
|
58
|
+
async storeCredentials(provider, tokenResponse, clientId, clientSecret) {
|
|
59
|
+
const credentials = await this.loadCredentials();
|
|
60
|
+
// Initialize provider array if it doesn't exist
|
|
61
|
+
if (!credentials[provider]) {
|
|
62
|
+
credentials[provider] = [];
|
|
63
|
+
}
|
|
64
|
+
const newCredential = {
|
|
65
|
+
accessToken: tokenResponse.access_token,
|
|
66
|
+
refreshToken: tokenResponse.refresh_token,
|
|
67
|
+
tokenType: tokenResponse.token_type,
|
|
68
|
+
expiresIn: tokenResponse.expires_in,
|
|
69
|
+
scope: tokenResponse.scope,
|
|
70
|
+
createdAt: tokenResponse.created_at,
|
|
71
|
+
clientId,
|
|
72
|
+
clientSecret,
|
|
73
|
+
// Include any additional fields from the token response
|
|
74
|
+
...Object.fromEntries(Object.entries(tokenResponse).filter(([key]) => ![
|
|
75
|
+
'access_token',
|
|
76
|
+
'refresh_token',
|
|
77
|
+
'token_type',
|
|
78
|
+
'expires_in',
|
|
79
|
+
'scope',
|
|
80
|
+
'created_at',
|
|
81
|
+
'provider',
|
|
82
|
+
].includes(key))),
|
|
83
|
+
};
|
|
84
|
+
// Add new credential to the array
|
|
85
|
+
credentials[provider].push(newCredential);
|
|
86
|
+
await this.saveCredentials(credentials);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Update stored credentials directly (used for token refresh)
|
|
90
|
+
*/
|
|
91
|
+
async updateStoredCredentials(provider, updatedCredentials) {
|
|
92
|
+
const credentials = await this.loadCredentials();
|
|
93
|
+
if (!credentials[provider] || credentials[provider].length === 0) {
|
|
94
|
+
credentials[provider] = [updatedCredentials];
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
credentials[provider][0] = updatedCredentials;
|
|
98
|
+
}
|
|
99
|
+
await this.saveCredentials(credentials);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Retrieve stored credentials for a provider (returns most recent credential)
|
|
103
|
+
*/
|
|
104
|
+
async getCredentials(provider) {
|
|
105
|
+
const credentials = await this.loadCredentials();
|
|
106
|
+
const providerCredentials = credentials[provider];
|
|
107
|
+
return providerCredentials && providerCredentials.length > 0
|
|
108
|
+
? (providerCredentials.at(-1) ?? null)
|
|
109
|
+
: null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Retrieve all stored credentials for a provider
|
|
113
|
+
*/
|
|
114
|
+
async getAllCredentials(provider) {
|
|
115
|
+
const credentials = await this.loadCredentials();
|
|
116
|
+
return credentials[provider] || [];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Remove credentials for a provider
|
|
120
|
+
*/
|
|
121
|
+
async removeCredentials(provider) {
|
|
122
|
+
const credentials = await this.loadCredentials();
|
|
123
|
+
delete credentials[provider];
|
|
124
|
+
await this.saveCredentials(credentials);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if stored access token is expired
|
|
128
|
+
*/
|
|
129
|
+
isTokenExpired(credentials) {
|
|
130
|
+
if (!credentials.expiresIn) {
|
|
131
|
+
return false; // No expiration info, assume valid
|
|
132
|
+
}
|
|
133
|
+
const createdAt = new Date(credentials.createdAt).getTime();
|
|
134
|
+
const expiresAt = createdAt + credentials.expiresIn * 1000;
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
// Consider expired if less than 5 minutes remaining
|
|
137
|
+
const bufferTime = 5 * 60 * 1000; // 5 minutes
|
|
138
|
+
return now >= expiresAt - bufferTime;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get valid access token, checking expiration
|
|
142
|
+
*/
|
|
143
|
+
async getValidToken(provider) {
|
|
144
|
+
const credentials = await this.getCredentials(provider);
|
|
145
|
+
if (!credentials) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
// If token is not expired, return it
|
|
149
|
+
if (!this.isTokenExpired(credentials)) {
|
|
150
|
+
return credentials.accessToken;
|
|
151
|
+
}
|
|
152
|
+
// Token is expired - try to refresh if we have refresh token and client secret
|
|
153
|
+
if (credentials.refreshToken && credentials.clientId && credentials.clientSecret) {
|
|
154
|
+
try {
|
|
155
|
+
const refreshedToken = await this.refreshToken(provider, credentials);
|
|
156
|
+
if (refreshedToken) {
|
|
157
|
+
return refreshedToken;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.warn(`Failed to refresh ${provider} token:`, error instanceof Error ? error.message : error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Refresh an expired token using the refresh token
|
|
168
|
+
*/
|
|
169
|
+
async refreshToken(provider, credentials) {
|
|
170
|
+
if (!credentials.refreshToken || !credentials.clientId || !credentials.clientSecret) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const refreshUrl = this.getRefreshUrl(provider);
|
|
174
|
+
if (!refreshUrl) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const response = await fetch(refreshUrl, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
182
|
+
Authorization: `Basic ${Buffer.from(`${credentials.clientId}:${credentials.clientSecret}`).toString('base64')}`,
|
|
183
|
+
},
|
|
184
|
+
body: new URLSearchParams({
|
|
185
|
+
grant_type: 'refresh_token',
|
|
186
|
+
refresh_token: credentials.refreshToken,
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
const errorText = await response.text();
|
|
191
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
192
|
+
}
|
|
193
|
+
const tokenData = (await response.json());
|
|
194
|
+
// Update stored credentials with new token
|
|
195
|
+
const updatedCredentials = {
|
|
196
|
+
...credentials,
|
|
197
|
+
accessToken: tokenData.access_token,
|
|
198
|
+
refreshToken: tokenData.refresh_token || credentials.refreshToken,
|
|
199
|
+
expiresIn: tokenData.expires_in,
|
|
200
|
+
createdAt: new Date().toISOString(),
|
|
201
|
+
};
|
|
202
|
+
await this.updateStoredCredentials(provider, updatedCredentials);
|
|
203
|
+
return tokenData.access_token;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
throw new Error(`Token refresh failed: ${error instanceof Error ? error.message : error}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Get the token refresh URL for a specific provider
|
|
211
|
+
*/
|
|
212
|
+
getRefreshUrl(provider) {
|
|
213
|
+
const refreshUrls = {
|
|
214
|
+
spotify: 'https://accounts.spotify.com/api/token',
|
|
215
|
+
// Google's token endpoint accepts client_secret_basic, so the shared
|
|
216
|
+
// Basic-auth refresh above works for it.
|
|
217
|
+
youtube: 'https://oauth2.googleapis.com/token',
|
|
218
|
+
};
|
|
219
|
+
return refreshUrls[provider] || null;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* List all providers with stored credentials
|
|
223
|
+
*/
|
|
224
|
+
async listProviders() {
|
|
225
|
+
const credentials = await this.loadCredentials();
|
|
226
|
+
return Object.keys(credentials);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Check if credentials exist for a provider
|
|
230
|
+
*/
|
|
231
|
+
async hasCredentials(provider) {
|
|
232
|
+
const credentials = await this.getCredentials(provider);
|
|
233
|
+
return credentials !== null;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get the path where credentials are stored (for user reference)
|
|
237
|
+
*/
|
|
238
|
+
getCredentialsPath() {
|
|
239
|
+
return this.credentialsPath;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { OAuthParams, TokenResponse, OAuthConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Base OAuth2 authorization-code flow. A standard provider only declares the
|
|
4
|
+
* endpoints, scopes, and credential style:
|
|
5
|
+
*
|
|
6
|
+
* class MyProvider extends OAuthProvider {
|
|
7
|
+
* static override providerId = 'my-source';
|
|
8
|
+
* static override authorizationUrl = 'https://example.com/authorize';
|
|
9
|
+
* static override tokenUrl = 'https://example.com/token';
|
|
10
|
+
* static override scopes = ['read'];
|
|
11
|
+
* static override tokenAuthStyle = 'basic-header'; // or 'body' (default)
|
|
12
|
+
* static override extraAuthParams = { access_type: 'offline' };
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* A provider whose protocol deviates from OAuth2 (e.g. Last.fm's signed
|
|
16
|
+
* auth.getSession) overrides buildAuthUrl / exchangeCodeForToken instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare abstract class OAuthProvider {
|
|
19
|
+
static providerId: string;
|
|
20
|
+
static authorizationUrl: string;
|
|
21
|
+
static tokenUrl: string;
|
|
22
|
+
static scopes: string[];
|
|
23
|
+
static requiresClientSecret: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Where the client credentials go on the token exchange: 'basic-header'
|
|
26
|
+
* sends an Authorization: Basic header; 'body' (default) puts
|
|
27
|
+
* client_id/client_secret in the form body.
|
|
28
|
+
*/
|
|
29
|
+
static tokenAuthStyle: 'basic-header' | 'body';
|
|
30
|
+
/** Provider-specific additions to the authorization URL. */
|
|
31
|
+
static extraAuthParams: Record<string, string>;
|
|
32
|
+
protected params: OAuthParams;
|
|
33
|
+
constructor(params: OAuthParams);
|
|
34
|
+
static getConfig(): OAuthConfig;
|
|
35
|
+
private get providerClass();
|
|
36
|
+
/**
|
|
37
|
+
* Build the authorization URL where the user will be redirected
|
|
38
|
+
*/
|
|
39
|
+
buildAuthUrl(): string;
|
|
40
|
+
/**
|
|
41
|
+
* Exchange authorization code for access tokens
|
|
42
|
+
*/
|
|
43
|
+
exchangeCodeForToken(code: string): Promise<TokenResponse>;
|
|
44
|
+
/** One POST to the token endpoint; overridable so tests can mock HTTP. */
|
|
45
|
+
protected postToken(url: string, data: URLSearchParams, config: {
|
|
46
|
+
headers: Record<string, string>;
|
|
47
|
+
}): Promise<any>;
|
|
48
|
+
/**
|
|
49
|
+
* Generate a random state parameter for CSRF protection
|
|
50
|
+
*/
|
|
51
|
+
protected generateState(): string;
|
|
52
|
+
/**
|
|
53
|
+
* Build query string from parameters
|
|
54
|
+
*/
|
|
55
|
+
protected buildQueryString(params: Record<string, string>): string;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=OAuthProvider.d.ts.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
/**
|
|
3
|
+
* Base OAuth2 authorization-code flow. A standard provider only declares the
|
|
4
|
+
* endpoints, scopes, and credential style:
|
|
5
|
+
*
|
|
6
|
+
* class MyProvider extends OAuthProvider {
|
|
7
|
+
* static override providerId = 'my-source';
|
|
8
|
+
* static override authorizationUrl = 'https://example.com/authorize';
|
|
9
|
+
* static override tokenUrl = 'https://example.com/token';
|
|
10
|
+
* static override scopes = ['read'];
|
|
11
|
+
* static override tokenAuthStyle = 'basic-header'; // or 'body' (default)
|
|
12
|
+
* static override extraAuthParams = { access_type: 'offline' };
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* A provider whose protocol deviates from OAuth2 (e.g. Last.fm's signed
|
|
16
|
+
* auth.getSession) overrides buildAuthUrl / exchangeCodeForToken instead.
|
|
17
|
+
*/
|
|
18
|
+
export class OAuthProvider {
|
|
19
|
+
static providerId;
|
|
20
|
+
static authorizationUrl;
|
|
21
|
+
static tokenUrl;
|
|
22
|
+
static scopes = [];
|
|
23
|
+
static requiresClientSecret = true;
|
|
24
|
+
/**
|
|
25
|
+
* Where the client credentials go on the token exchange: 'basic-header'
|
|
26
|
+
* sends an Authorization: Basic header; 'body' (default) puts
|
|
27
|
+
* client_id/client_secret in the form body.
|
|
28
|
+
*/
|
|
29
|
+
static tokenAuthStyle = 'body';
|
|
30
|
+
/** Provider-specific additions to the authorization URL. */
|
|
31
|
+
static extraAuthParams = {};
|
|
32
|
+
params;
|
|
33
|
+
constructor(params) {
|
|
34
|
+
this.params = params;
|
|
35
|
+
}
|
|
36
|
+
static getConfig() {
|
|
37
|
+
return {
|
|
38
|
+
providerId: this.providerId,
|
|
39
|
+
authorizationUrl: this.authorizationUrl,
|
|
40
|
+
tokenUrl: this.tokenUrl,
|
|
41
|
+
scopes: this.scopes,
|
|
42
|
+
requiresClientSecret: this.requiresClientSecret,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
get providerClass() {
|
|
46
|
+
return this.constructor;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build the authorization URL where the user will be redirected
|
|
50
|
+
*/
|
|
51
|
+
buildAuthUrl() {
|
|
52
|
+
const cls = this.providerClass;
|
|
53
|
+
const params = {
|
|
54
|
+
client_id: this.params.clientId,
|
|
55
|
+
response_type: 'code',
|
|
56
|
+
redirect_uri: this.params.redirectUri,
|
|
57
|
+
};
|
|
58
|
+
const scope = (this.params.scopes?.length ? this.params.scopes : cls.scopes).join(' ');
|
|
59
|
+
if (scope) {
|
|
60
|
+
params.scope = scope;
|
|
61
|
+
}
|
|
62
|
+
Object.assign(params, cls.extraAuthParams);
|
|
63
|
+
if (this.params.state) {
|
|
64
|
+
params.state = this.params.state;
|
|
65
|
+
}
|
|
66
|
+
return `${cls.authorizationUrl}?${this.buildQueryString(params)}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Exchange authorization code for access tokens
|
|
70
|
+
*/
|
|
71
|
+
async exchangeCodeForToken(code) {
|
|
72
|
+
const cls = this.providerClass;
|
|
73
|
+
try {
|
|
74
|
+
const data = new URLSearchParams({
|
|
75
|
+
grant_type: 'authorization_code',
|
|
76
|
+
code,
|
|
77
|
+
redirect_uri: this.params.redirectUri,
|
|
78
|
+
});
|
|
79
|
+
const headers = {
|
|
80
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
81
|
+
};
|
|
82
|
+
if (cls.tokenAuthStyle === 'basic-header') {
|
|
83
|
+
const credentials = Buffer.from(`${this.params.clientId}:${this.params.clientSecret}`).toString('base64');
|
|
84
|
+
headers.Authorization = `Basic ${credentials}`;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
data.set('client_id', this.params.clientId);
|
|
88
|
+
data.set('client_secret', this.params.clientSecret);
|
|
89
|
+
}
|
|
90
|
+
const response = await this.postToken(cls.tokenUrl, data, { headers });
|
|
91
|
+
return {
|
|
92
|
+
provider: cls.providerId,
|
|
93
|
+
access_token: response.access_token,
|
|
94
|
+
refresh_token: response.refresh_token,
|
|
95
|
+
// Some providers (foursquare) omit token_type on the exchange
|
|
96
|
+
token_type: response.token_type ?? 'Bearer',
|
|
97
|
+
expires_in: response.expires_in,
|
|
98
|
+
scope: response.scope,
|
|
99
|
+
created_at: new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (axios.isAxiosError(error)) {
|
|
104
|
+
const message = error.response?.data?.error_description || error.response?.data?.error || error.message;
|
|
105
|
+
throw new Error(`Failed to exchange code for tokens: ${message}`);
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** One POST to the token endpoint; overridable so tests can mock HTTP. */
|
|
111
|
+
async postToken(url, data, config) {
|
|
112
|
+
const response = await axios.post(url, data, config);
|
|
113
|
+
return response.data;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Generate a random state parameter for CSRF protection
|
|
117
|
+
*/
|
|
118
|
+
generateState() {
|
|
119
|
+
return Math.random().toString(36).slice(2, 15) + Math.random().toString(36).slice(2, 15);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build query string from parameters
|
|
123
|
+
*/
|
|
124
|
+
buildQueryString(params) {
|
|
125
|
+
return Object.entries(params)
|
|
126
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
127
|
+
.join('&');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AuthorizationResult } from './types.js';
|
|
2
|
+
/** A loopback-only callback server. Its pending callback always settles on shutdown. */
|
|
3
|
+
export declare class OAuthServer {
|
|
4
|
+
private server?;
|
|
5
|
+
private port?;
|
|
6
|
+
private timeout?;
|
|
7
|
+
private result?;
|
|
8
|
+
private failure?;
|
|
9
|
+
private resolve?;
|
|
10
|
+
private reject?;
|
|
11
|
+
start(preferredPort?: number, timeoutMs?: number): Promise<number>;
|
|
12
|
+
waitForCallback(): Promise<AuthorizationResult>;
|
|
13
|
+
stop(error?: Error): void;
|
|
14
|
+
getCallbackUrl(): string;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=OAuthServer.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
/** A loopback-only callback server. Its pending callback always settles on shutdown. */
|
|
3
|
+
export class OAuthServer {
|
|
4
|
+
server;
|
|
5
|
+
port;
|
|
6
|
+
timeout;
|
|
7
|
+
result;
|
|
8
|
+
failure;
|
|
9
|
+
resolve;
|
|
10
|
+
reject;
|
|
11
|
+
async start(preferredPort = 0, timeoutMs = 5 * 60 * 1000) {
|
|
12
|
+
if (this.server)
|
|
13
|
+
throw new Error('OAuth server already started');
|
|
14
|
+
this.result = undefined;
|
|
15
|
+
this.failure = undefined;
|
|
16
|
+
const server = http.createServer((request, response) => {
|
|
17
|
+
const url = new URL(request.url || '/', 'http://127.0.0.1');
|
|
18
|
+
if (url.pathname !== '/callback') {
|
|
19
|
+
response.writeHead(url.pathname === '/' ? 200 : 404, { 'Content-Type': 'text/plain' });
|
|
20
|
+
response.end(url.pathname === '/' ? 'Waiting for authorization.' : 'Not found');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const result = {};
|
|
24
|
+
for (const key of ['code', 'token', 'state', 'error', 'error_description']) {
|
|
25
|
+
const value = url.searchParams.get(key);
|
|
26
|
+
if (value !== null)
|
|
27
|
+
result[key] = value;
|
|
28
|
+
}
|
|
29
|
+
this.result = result;
|
|
30
|
+
response.writeHead(result.error ? 400 : 200, { 'Content-Type': 'text/plain' });
|
|
31
|
+
response.end(result.error
|
|
32
|
+
? 'Authorization failed. Return to your terminal.'
|
|
33
|
+
: 'Authorization received. Return to your terminal.');
|
|
34
|
+
this.resolve?.(result);
|
|
35
|
+
});
|
|
36
|
+
this.server = server;
|
|
37
|
+
try {
|
|
38
|
+
await new Promise((resolve, reject) => {
|
|
39
|
+
server.once('error', reject);
|
|
40
|
+
server.listen(preferredPort, '127.0.0.1', () => {
|
|
41
|
+
server.removeListener('error', reject);
|
|
42
|
+
resolve();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
const address = server.address();
|
|
46
|
+
if (!address || typeof address === 'string')
|
|
47
|
+
throw new Error('Failed to start OAuth server');
|
|
48
|
+
this.port = address.port;
|
|
49
|
+
this.timeout = setTimeout(() => this.stop(new Error('OAuth authorization timed out')), timeoutMs);
|
|
50
|
+
return this.port;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
this.stop();
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async waitForCallback() {
|
|
58
|
+
if (this.result)
|
|
59
|
+
return this.result;
|
|
60
|
+
if (this.failure)
|
|
61
|
+
throw this.failure;
|
|
62
|
+
if (!this.server)
|
|
63
|
+
throw new Error('Server not started');
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
this.resolve = resolve;
|
|
66
|
+
this.reject = reject;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
stop(error = new Error('OAuth authorization cancelled')) {
|
|
70
|
+
if (this.timeout)
|
|
71
|
+
clearTimeout(this.timeout);
|
|
72
|
+
this.timeout = undefined;
|
|
73
|
+
if (!this.result) {
|
|
74
|
+
this.failure = error;
|
|
75
|
+
this.reject?.(error);
|
|
76
|
+
}
|
|
77
|
+
this.resolve = undefined;
|
|
78
|
+
this.reject = undefined;
|
|
79
|
+
this.server?.closeAllConnections();
|
|
80
|
+
this.server?.close();
|
|
81
|
+
this.server = undefined;
|
|
82
|
+
this.port = undefined;
|
|
83
|
+
}
|
|
84
|
+
getCallbackUrl() {
|
|
85
|
+
if (!this.port)
|
|
86
|
+
throw new Error('Server not started');
|
|
87
|
+
return `http://127.0.0.1:${this.port}/callback`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { OAuthProvider } from './OAuthProvider.js';
|
|
2
|
+
export declare class OAuthProviderRegistry {
|
|
3
|
+
private static providers;
|
|
4
|
+
/**
|
|
5
|
+
* Register an OAuth provider
|
|
6
|
+
*/
|
|
7
|
+
static register(provider: typeof OAuthProvider): void;
|
|
8
|
+
/**
|
|
9
|
+
* Get a provider by ID
|
|
10
|
+
*/
|
|
11
|
+
static get(providerId: string): typeof OAuthProvider | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Get all registered provider IDs
|
|
14
|
+
*/
|
|
15
|
+
static getProviderIds(): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Check if a provider is registered
|
|
18
|
+
*/
|
|
19
|
+
static has(providerId: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Get all providers
|
|
22
|
+
*/
|
|
23
|
+
static getAll(): Map<string, typeof OAuthProvider>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=ProviderRegistry.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class OAuthProviderRegistry {
|
|
2
|
+
static providers = new Map();
|
|
3
|
+
/**
|
|
4
|
+
* Register an OAuth provider
|
|
5
|
+
*/
|
|
6
|
+
static register(provider) {
|
|
7
|
+
if (!provider.providerId) {
|
|
8
|
+
throw new Error(`Provider must have a providerId: ${provider.name}`);
|
|
9
|
+
}
|
|
10
|
+
this.providers.set(provider.providerId, provider);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get a provider by ID
|
|
14
|
+
*/
|
|
15
|
+
static get(providerId) {
|
|
16
|
+
return this.providers.get(providerId);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get all registered provider IDs
|
|
20
|
+
*/
|
|
21
|
+
static getProviderIds() {
|
|
22
|
+
return [...this.providers.keys()];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check if a provider is registered
|
|
26
|
+
*/
|
|
27
|
+
static has(providerId) {
|
|
28
|
+
return this.providers.has(providerId);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get all providers
|
|
32
|
+
*/
|
|
33
|
+
static getAll() {
|
|
34
|
+
return new Map(this.providers);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const TokenHelper: {
|
|
2
|
+
/**
|
|
3
|
+
* Get a valid access token for a provider, with user-friendly error messages
|
|
4
|
+
*/
|
|
5
|
+
getValidToken(provider: string): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Get token or use provided access token, with preference for keychain
|
|
8
|
+
*/
|
|
9
|
+
getTokenWithFallback(provider: string, providedToken?: string): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Check if valid credentials exist for a provider
|
|
12
|
+
*/
|
|
13
|
+
hasValidCredentials(provider: string): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Remove stored credentials (for logout/reset)
|
|
16
|
+
*/
|
|
17
|
+
removeCredentials(provider: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* List providers with stored credentials
|
|
20
|
+
*/
|
|
21
|
+
listProvidersWithCredentials(): Promise<string[]>;
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=TokenHelper.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CredentialManager } from './CredentialManager.js';
|
|
2
|
+
export const TokenHelper = {
|
|
3
|
+
/**
|
|
4
|
+
* Get a valid access token for a provider, with user-friendly error messages
|
|
5
|
+
*/
|
|
6
|
+
async getValidToken(provider) {
|
|
7
|
+
const token = await CredentialManager.getValidToken(provider);
|
|
8
|
+
if (!token) {
|
|
9
|
+
const hasCredentials = await CredentialManager.hasCredentials(provider);
|
|
10
|
+
if (hasCredentials) {
|
|
11
|
+
throw new Error(`${provider} credentials are expired and could not be refreshed. Please re-authenticate:\n` +
|
|
12
|
+
` chronicle auth ${provider} --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET`);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
throw new Error(`No ${provider} credentials found. Please authenticate first:\n` +
|
|
16
|
+
` chronicle auth ${provider} --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return token;
|
|
20
|
+
},
|
|
21
|
+
/**
|
|
22
|
+
* Get token or use provided access token, with preference for keychain
|
|
23
|
+
*/
|
|
24
|
+
async getTokenWithFallback(provider, providedToken) {
|
|
25
|
+
// If user provided a token explicitly, use it
|
|
26
|
+
if (providedToken) {
|
|
27
|
+
return providedToken;
|
|
28
|
+
}
|
|
29
|
+
// Otherwise try to get from keychain
|
|
30
|
+
return this.getValidToken(provider);
|
|
31
|
+
},
|
|
32
|
+
/**
|
|
33
|
+
* Check if valid credentials exist for a provider
|
|
34
|
+
*/
|
|
35
|
+
async hasValidCredentials(provider) {
|
|
36
|
+
try {
|
|
37
|
+
const token = await CredentialManager.getValidToken(provider);
|
|
38
|
+
return token !== null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Remove stored credentials (for logout/reset)
|
|
46
|
+
*/
|
|
47
|
+
async removeCredentials(provider) {
|
|
48
|
+
await CredentialManager.removeCredentials(provider);
|
|
49
|
+
},
|
|
50
|
+
/**
|
|
51
|
+
* List providers with stored credentials
|
|
52
|
+
*/
|
|
53
|
+
async listProvidersWithCredentials() {
|
|
54
|
+
return CredentialManager.listProviders();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directory Chronicle keeps its config and credentials in — the same one
|
|
3
|
+
* oclif resolves for `Config.configDir`, so code running outside an oclif
|
|
4
|
+
* command (the agent harness, plugins' own tooling) reads the files that
|
|
5
|
+
* `chronicle config set` and `chronicle auth set` write. `CHRONICLE_CONFIG_DIR`
|
|
6
|
+
* is oclif's scoped override, then XDG_CONFIG_HOME / LOCALAPPDATA (Windows) /
|
|
7
|
+
* ~/.config, mirroring `@oclif/core` `Config.dir('config')`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function chronicleConfigDir(): string;
|
|
10
|
+
//# sourceMappingURL=configDir.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* The directory Chronicle keeps its config and credentials in — the same one
|
|
5
|
+
* oclif resolves for `Config.configDir`, so code running outside an oclif
|
|
6
|
+
* command (the agent harness, plugins' own tooling) reads the files that
|
|
7
|
+
* `chronicle config set` and `chronicle auth set` write. `CHRONICLE_CONFIG_DIR`
|
|
8
|
+
* is oclif's scoped override, then XDG_CONFIG_HOME / LOCALAPPDATA (Windows) /
|
|
9
|
+
* ~/.config, mirroring `@oclif/core` `Config.dir('config')`.
|
|
10
|
+
*/
|
|
11
|
+
export function chronicleConfigDir() {
|
|
12
|
+
return (process.env.CHRONICLE_CONFIG_DIR ||
|
|
13
|
+
join(process.env.XDG_CONFIG_HOME ||
|
|
14
|
+
(process.platform === 'win32' && process.env.LOCALAPPDATA) ||
|
|
15
|
+
join(homedir(), '.config'), 'chronicle'));
|
|
16
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './CredentialManager.js';
|
|
2
|
+
export { FileCredentialManager } from './FileCredentialManager.js';
|
|
3
|
+
export * from './OAuthProvider.js';
|
|
4
|
+
export * from './ProviderRegistry.js';
|
|
5
|
+
export * from './OAuthServer.js';
|
|
6
|
+
export * from './TokenHelper.js';
|
|
7
|
+
export * from './resolveCredentials.js';
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export * from './configDir.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './CredentialManager.js';
|
|
2
|
+
export { FileCredentialManager } from './FileCredentialManager.js';
|
|
3
|
+
export * from './OAuthProvider.js';
|
|
4
|
+
export * from './ProviderRegistry.js';
|
|
5
|
+
export * from './OAuthServer.js';
|
|
6
|
+
export * from './TokenHelper.js';
|
|
7
|
+
export * from './resolveCredentials.js';
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export * from './configDir.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { StoredCredentials } from './CredentialManager.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shared credential resolution for API plugins. Consolidates the per-plugin
|
|
4
|
+
* pattern of "flag/constructor value, else stored credential under one of a
|
|
5
|
+
* few historical field names, else a source-specific error".
|
|
6
|
+
*
|
|
7
|
+
* This is the CredentialManager (stored-secret) path; OAuth sources that need
|
|
8
|
+
* refresh go through TokenHelper.getValidToken instead.
|
|
9
|
+
*/
|
|
10
|
+
export interface CredentialFieldSpec {
|
|
11
|
+
/** StoredCredentials fields tried in order until one has a value. */
|
|
12
|
+
from: Array<keyof StoredCredentials | string>;
|
|
13
|
+
/** Optional fields resolve to undefined instead of failing the lookup. */
|
|
14
|
+
optional?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ResolveCredentialsOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Already-resolved values (CLI flags, constructor args). A non-empty
|
|
19
|
+
* override wins without consulting the store.
|
|
20
|
+
*/
|
|
21
|
+
overrides?: Record<string, string | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* Full replacement for the missing-credential error, so a plugin keeps its
|
|
24
|
+
* exact user-facing message (auth command, credential-creation URL, …).
|
|
25
|
+
*/
|
|
26
|
+
errorMessage?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Pure resolution core, separated for testing. */
|
|
29
|
+
export declare function pickCredentialFields<F extends string>(provider: string, stored: object | null, fields: Record<F, CredentialFieldSpec>, options?: ResolveCredentialsOptions): Record<F, string | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a source's credentials: overrides first, then the stored
|
|
32
|
+
* credentials under each field's alias chain. Throws when a required field
|
|
33
|
+
* is missing everywhere.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveCredentials<F extends string>(provider: string, fields: Record<F, CredentialFieldSpec>, options?: ResolveCredentialsOptions): Promise<Record<F, string | undefined>>;
|
|
36
|
+
//# sourceMappingURL=resolveCredentials.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { CredentialManager } from './CredentialManager.js';
|
|
2
|
+
/** Pure resolution core, separated for testing. */
|
|
3
|
+
export function pickCredentialFields(provider, stored, fields, options = {}) {
|
|
4
|
+
const resolved = {};
|
|
5
|
+
const missing = [];
|
|
6
|
+
const storedFields = (stored ?? {});
|
|
7
|
+
for (const name of Object.keys(fields)) {
|
|
8
|
+
const spec = fields[name];
|
|
9
|
+
const override = options.overrides?.[name];
|
|
10
|
+
let value = override || undefined;
|
|
11
|
+
if (value === undefined && stored) {
|
|
12
|
+
for (const alias of spec.from) {
|
|
13
|
+
const candidate = storedFields[alias];
|
|
14
|
+
if (typeof candidate === 'string' && candidate !== '') {
|
|
15
|
+
value = candidate;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
resolved[name] = value;
|
|
21
|
+
if (value === undefined && !spec.optional)
|
|
22
|
+
missing.push(name);
|
|
23
|
+
}
|
|
24
|
+
if (missing.length > 0) {
|
|
25
|
+
throw new Error(options.errorMessage ??
|
|
26
|
+
`${provider} credentials are missing ${missing.join(', ')}. Authenticate with: chronicle auth ${provider}`);
|
|
27
|
+
}
|
|
28
|
+
return resolved;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a source's credentials: overrides first, then the stored
|
|
32
|
+
* credentials under each field's alias chain. Throws when a required field
|
|
33
|
+
* is missing everywhere.
|
|
34
|
+
*/
|
|
35
|
+
export async function resolveCredentials(provider, fields, options = {}) {
|
|
36
|
+
let stored = null;
|
|
37
|
+
try {
|
|
38
|
+
stored = await CredentialManager.getCredentials(provider);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// No stored credentials — overrides may still satisfy the spec.
|
|
42
|
+
}
|
|
43
|
+
return pickCredentialFields(provider, stored, fields, options);
|
|
44
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface OAuthParams {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
scopes?: string[];
|
|
6
|
+
state?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TokenResponse {
|
|
9
|
+
provider: string;
|
|
10
|
+
access_token: string;
|
|
11
|
+
refresh_token?: string;
|
|
12
|
+
token_type: string;
|
|
13
|
+
expires_in?: number;
|
|
14
|
+
scope?: string;
|
|
15
|
+
created_at: string;
|
|
16
|
+
username?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface OAuthConfig {
|
|
19
|
+
providerId: string;
|
|
20
|
+
authorizationUrl: string;
|
|
21
|
+
tokenUrl: string;
|
|
22
|
+
scopes: string[];
|
|
23
|
+
requiresClientSecret: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface AuthorizationResult {
|
|
26
|
+
code?: string;
|
|
27
|
+
token?: string;
|
|
28
|
+
state?: string;
|
|
29
|
+
error?: string;
|
|
30
|
+
error_description?: string;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chronicle.app/auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/**/*.js",
|
|
8
|
+
"dist/**/*.d.ts"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -b tsconfig.lib.json",
|
|
12
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
13
|
+
"lint": "eslint src --ext .ts,.js",
|
|
14
|
+
"test": "node --test src/*.test.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"author": "Andrew Louis",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"description": "Standalone Chronicle credential and OAuth services",
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@chronicle.app/tsconfig": "0.1.0",
|
|
23
|
+
"@types/node": "^22.10.1",
|
|
24
|
+
"typescript": "^5.7.2"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://chronicle.app",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/chronicle-app/chronicle/issues/new?labels=core:etl&title=[etl]%20"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/chronicle-app/chronicle.git",
|
|
33
|
+
"directory": "core/auth"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"registry": "https://registry.npmjs.org/"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"axios": "^1.7.9"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22.13.0"
|
|
44
|
+
},
|
|
45
|
+
"exports": {
|
|
46
|
+
".": {
|
|
47
|
+
"types": "./dist/index.d.ts",
|
|
48
|
+
"import": "./dist/index.js"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|