@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/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
+ }