@papercraneai/cli 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/LICENSE +201 -0
- package/README.md +172 -0
- package/bin/papercrane.js +209 -0
- package/lib/callback-server.js +92 -0
- package/lib/cloud-client.js +193 -0
- package/lib/config.js +97 -0
- package/lib/facebook-auth.js +148 -0
- package/lib/facebook-callback-server.js +105 -0
- package/lib/function-client.js +451 -0
- package/lib/google-auth.js +134 -0
- package/lib/list-credentials.js +116 -0
- package/lib/pkce.js +55 -0
- package/lib/storage.js +234 -0
- package/package.json +30 -0
package/lib/pkce.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a cryptographically random code verifier for PKCE
|
|
5
|
+
* @param {number} length - Length of the code verifier (43-128 characters)
|
|
6
|
+
* @returns {string} Base64URL-encoded code verifier
|
|
7
|
+
*/
|
|
8
|
+
export function generateCodeVerifier(length = 128) {
|
|
9
|
+
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
10
|
+
const randomValues = crypto.randomBytes(length);
|
|
11
|
+
let result = '';
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < length; i++) {
|
|
14
|
+
result += charset[randomValues[i] % charset.length];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generates a code challenge from a code verifier using SHA256
|
|
22
|
+
* @param {string} verifier - The code verifier
|
|
23
|
+
* @returns {string} Base64URL-encoded SHA256 hash of the verifier
|
|
24
|
+
*/
|
|
25
|
+
export function generateCodeChallenge(verifier) {
|
|
26
|
+
const hash = crypto.createHash('sha256').update(verifier).digest();
|
|
27
|
+
return base64URLEncode(hash);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Base64URL encodes a buffer
|
|
32
|
+
* @param {Buffer} buffer - Buffer to encode
|
|
33
|
+
* @returns {string} Base64URL-encoded string
|
|
34
|
+
*/
|
|
35
|
+
function base64URLEncode(buffer) {
|
|
36
|
+
return buffer
|
|
37
|
+
.toString('base64')
|
|
38
|
+
.replace(/\+/g, '-')
|
|
39
|
+
.replace(/\//g, '_')
|
|
40
|
+
.replace(/=/g, '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generates both code verifier and challenge for PKCE flow
|
|
45
|
+
* @returns {{verifier: string, challenge: string}} PKCE credentials
|
|
46
|
+
*/
|
|
47
|
+
export function generatePKCE() {
|
|
48
|
+
const verifier = generateCodeVerifier();
|
|
49
|
+
const challenge = generateCodeChallenge(verifier);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
verifier,
|
|
53
|
+
challenge
|
|
54
|
+
};
|
|
55
|
+
}
|
package/lib/storage.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
|
|
7
|
+
const PAPERCRANE_DIR = path.join(os.homedir(), '.papercrane');
|
|
8
|
+
const GOOGLE_DIR = path.join(PAPERCRANE_DIR, 'google');
|
|
9
|
+
const FACEBOOK_DIR = path.join(PAPERCRANE_DIR, 'facebook');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ensures the storage directories exist
|
|
13
|
+
*/
|
|
14
|
+
export async function ensureStorageDirectories() {
|
|
15
|
+
await fs.mkdir(PAPERCRANE_DIR, { recursive: true });
|
|
16
|
+
await fs.mkdir(GOOGLE_DIR, { recursive: true });
|
|
17
|
+
await fs.mkdir(FACEBOOK_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Saves Google OAuth credentials in Google SDK-compatible format
|
|
22
|
+
* @param {Object} credentials - The credentials to save
|
|
23
|
+
* @param {string} credentials.access_token - Access token
|
|
24
|
+
* @param {string} credentials.refresh_token - Refresh token
|
|
25
|
+
* @param {number} credentials.expires_in - Token expiration time in seconds
|
|
26
|
+
* @param {string} credentials.scope - Granted scopes
|
|
27
|
+
* @param {string} credentials.token_type - Token type
|
|
28
|
+
* @param {string} clientId - OAuth client ID
|
|
29
|
+
* @param {string} clientSecret - OAuth client secret
|
|
30
|
+
*/
|
|
31
|
+
export async function saveGoogleCredentials(credentials, clientId, clientSecret) {
|
|
32
|
+
await ensureStorageDirectories();
|
|
33
|
+
|
|
34
|
+
const timestamp = Date.now();
|
|
35
|
+
const expiresAt = timestamp + (credentials.expires_in * 1000);
|
|
36
|
+
|
|
37
|
+
// Format credentials for Google SDK compatibility
|
|
38
|
+
const credentialData = {
|
|
39
|
+
credential_id: randomUUID(),
|
|
40
|
+
type: 'authorized_user',
|
|
41
|
+
client_id: clientId,
|
|
42
|
+
client_secret: clientSecret,
|
|
43
|
+
refresh_token: credentials.refresh_token,
|
|
44
|
+
// Include additional fields for papercrane tracking
|
|
45
|
+
access_token: credentials.access_token,
|
|
46
|
+
expires_in: credentials.expires_in,
|
|
47
|
+
scope: credentials.scope,
|
|
48
|
+
token_type: credentials.token_type,
|
|
49
|
+
created_at: timestamp,
|
|
50
|
+
expires_at: expiresAt
|
|
51
|
+
// cloud_id: will be set after first push to cloud
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Generate a filename based on the scopes
|
|
55
|
+
// Sort scopes alphabetically and hash them to ensure consistency
|
|
56
|
+
const scopes = credentials.scope.split(' ').sort().join(' ');
|
|
57
|
+
const scopeHash = crypto.createHash('md5').update(scopes).digest('hex');
|
|
58
|
+
const filename = `credentials-${scopeHash}.json`;
|
|
59
|
+
const filepath = path.join(GOOGLE_DIR, filename);
|
|
60
|
+
|
|
61
|
+
await fs.writeFile(filepath, JSON.stringify(credentialData, null, 2));
|
|
62
|
+
|
|
63
|
+
return filepath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Lists all stored Google credentials
|
|
68
|
+
* @returns {Promise<Array<{file: string, data: Object}>>}
|
|
69
|
+
*/
|
|
70
|
+
export async function listGoogleCredentials() {
|
|
71
|
+
try {
|
|
72
|
+
await ensureStorageDirectories();
|
|
73
|
+
const files = await fs.readdir(GOOGLE_DIR);
|
|
74
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
75
|
+
|
|
76
|
+
const credentials = await Promise.all(
|
|
77
|
+
jsonFiles.map(async (file) => {
|
|
78
|
+
const filepath = path.join(GOOGLE_DIR, file);
|
|
79
|
+
const content = await fs.readFile(filepath, 'utf-8');
|
|
80
|
+
const data = JSON.parse(content);
|
|
81
|
+
return { file, filepath, data };
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return credentials;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error.code === 'ENOENT') {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gets the Google credentials directory path
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
export function getGoogleCredentialsDir() {
|
|
99
|
+
return GOOGLE_DIR;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Saves Facebook OAuth credentials
|
|
104
|
+
* @param {Object} credentials - The credentials to save
|
|
105
|
+
* @param {string} credentials.access_token - Access token
|
|
106
|
+
* @param {number} credentials.expires_in - Token expiration time in seconds
|
|
107
|
+
* @param {string} credentials.token_type - Token type
|
|
108
|
+
* @param {string} appId - Facebook App ID
|
|
109
|
+
* @param {string} scope - Granted scopes (comma-separated)
|
|
110
|
+
*/
|
|
111
|
+
export async function saveFacebookCredentials(credentials, appId, scope) {
|
|
112
|
+
await ensureStorageDirectories();
|
|
113
|
+
|
|
114
|
+
const timestamp = Date.now();
|
|
115
|
+
const expiresAt = timestamp + (credentials.expires_in * 1000);
|
|
116
|
+
|
|
117
|
+
// Format credentials for Facebook SDK compatibility
|
|
118
|
+
const credentialData = {
|
|
119
|
+
credential_id: randomUUID(),
|
|
120
|
+
access_token: credentials.access_token,
|
|
121
|
+
token_type: credentials.token_type || 'bearer',
|
|
122
|
+
expires_in: credentials.expires_in,
|
|
123
|
+
scope: scope,
|
|
124
|
+
app_id: appId,
|
|
125
|
+
created_at: timestamp,
|
|
126
|
+
expires_at: expiresAt
|
|
127
|
+
// cloud_id: will be set after first push to cloud
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Generate a filename based on the scopes
|
|
131
|
+
const scopeHash = crypto.createHash('md5').update(scope).digest('hex');
|
|
132
|
+
const filename = `credentials-${scopeHash}.json`;
|
|
133
|
+
const filepath = path.join(FACEBOOK_DIR, filename);
|
|
134
|
+
|
|
135
|
+
await fs.writeFile(filepath, JSON.stringify(credentialData, null, 2));
|
|
136
|
+
|
|
137
|
+
return filepath;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Lists all stored Facebook credentials
|
|
142
|
+
* @returns {Promise<Array<{file: string, data: Object}>>}
|
|
143
|
+
*/
|
|
144
|
+
export async function listFacebookCredentials() {
|
|
145
|
+
try {
|
|
146
|
+
await ensureStorageDirectories();
|
|
147
|
+
const files = await fs.readdir(FACEBOOK_DIR);
|
|
148
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
149
|
+
|
|
150
|
+
const credentials = await Promise.all(
|
|
151
|
+
jsonFiles.map(async (file) => {
|
|
152
|
+
const filepath = path.join(FACEBOOK_DIR, file);
|
|
153
|
+
const content = await fs.readFile(filepath, 'utf-8');
|
|
154
|
+
const data = JSON.parse(content);
|
|
155
|
+
return { file, filepath, data };
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return credentials;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error.code === 'ENOENT') {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Gets the Facebook credentials directory path
|
|
170
|
+
* @returns {string}
|
|
171
|
+
*/
|
|
172
|
+
export function getFacebookCredentialsDir() {
|
|
173
|
+
return FACEBOOK_DIR;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Update a credential file with cloud_id after successful push
|
|
178
|
+
* @param {string} filepath - Path to the credential file
|
|
179
|
+
* @param {string} cloudId - The cloud ID returned from the server
|
|
180
|
+
*/
|
|
181
|
+
export async function updateCredentialCloudId(filepath, cloudId) {
|
|
182
|
+
const content = await fs.readFile(filepath, 'utf-8');
|
|
183
|
+
const data = JSON.parse(content);
|
|
184
|
+
data.cloud_id = cloudId;
|
|
185
|
+
await fs.writeFile(filepath, JSON.stringify(data, null, 2));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if a credential with the given cloud_id exists locally
|
|
190
|
+
* @param {string} cloudId - The cloud ID to search for
|
|
191
|
+
* @returns {Promise<boolean>}
|
|
192
|
+
*/
|
|
193
|
+
export async function hasLocalCredentialWithCloudId(cloudId) {
|
|
194
|
+
const allCreds = await getAllLocalCredentials();
|
|
195
|
+
return allCreds.some(cred => cred.data.cloud_id === cloudId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get all local credentials (both Google and Facebook)
|
|
200
|
+
* @returns {Promise<Array<{provider: string, file: string, filepath: string, data: Object}>>}
|
|
201
|
+
*/
|
|
202
|
+
export async function getAllLocalCredentials() {
|
|
203
|
+
const googleCreds = await listGoogleCredentials();
|
|
204
|
+
const facebookCreds = await listFacebookCredentials();
|
|
205
|
+
|
|
206
|
+
return [
|
|
207
|
+
...googleCreds.map(c => ({ provider: 'google', ...c })),
|
|
208
|
+
...facebookCreds.map(c => ({ provider: 'facebook', ...c }))
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Save a credential pulled from cloud
|
|
214
|
+
* @param {string} provider - Provider name ('google' or 'facebook')
|
|
215
|
+
* @param {Object} credentialData - The credential data including cloud_id
|
|
216
|
+
* @returns {Promise<string>} Path to saved file
|
|
217
|
+
*/
|
|
218
|
+
export async function saveCloudCredential(provider, credentialData) {
|
|
219
|
+
await ensureStorageDirectories();
|
|
220
|
+
|
|
221
|
+
const dir = provider === 'google' ? GOOGLE_DIR : FACEBOOK_DIR;
|
|
222
|
+
const scope = credentialData.scope || '';
|
|
223
|
+
const scopeHash = crypto.createHash('md5').update(scope).digest('hex');
|
|
224
|
+
const filename = `credentials-${scopeHash}.json`;
|
|
225
|
+
const filepath = path.join(dir, filename);
|
|
226
|
+
|
|
227
|
+
// Ensure credential_id exists (generate if cloud doesn't have one)
|
|
228
|
+
if (!credentialData.credential_id) {
|
|
229
|
+
credentialData.credential_id = randomUUID();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await fs.writeFile(filepath, JSON.stringify(credentialData, null, 2));
|
|
233
|
+
return filepath;
|
|
234
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@papercraneai/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for managing OAuth credentials for LLM integrations",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"papercrane": "./bin/papercrane.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"lib"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node bin/papercrane.js"
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["oauth", "cli", "authentication", "google"],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"commander": "^12.0.0",
|
|
25
|
+
"axios": "^1.6.0",
|
|
26
|
+
"chalk": "^4.1.2",
|
|
27
|
+
"inquirer": "^8.2.6",
|
|
28
|
+
"open": "^8.4.2"
|
|
29
|
+
}
|
|
30
|
+
}
|