@zapier/zapier-sdk-cli 0.42.2 → 0.43.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 +12 -0
- package/dist/cli.cjs +619 -34
- package/dist/cli.d.mts +0 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.mjs +601 -19
- package/dist/index.cjs +607 -23
- package/dist/index.mjs +592 -11
- package/dist/login.cjs +599 -7
- package/dist/login.d.mts +144 -1
- package/dist/login.d.ts +144 -1
- package/dist/login.mjs +565 -1
- package/dist/package.json +5 -2
- package/dist/src/login/filesystem-cache.d.ts +25 -0
- package/dist/src/login/filesystem-cache.js +195 -0
- package/dist/src/login/index.d.ts +115 -0
- package/dist/src/login/index.js +442 -0
- package/dist/src/login/keychain.d.ts +18 -0
- package/dist/src/login/keychain.js +74 -0
- package/dist/src/login.d.ts +10 -1
- package/dist/src/login.js +10 -1
- package/dist/src/plugins/feedback/index.js +1 -1
- package/dist/src/plugins/getLoginConfigPath/index.js +1 -1
- package/dist/src/plugins/login/index.js +1 -1
- package/dist/src/plugins/logout/index.js +1 -1
- package/dist/src/sdk.js +1 -1
- package/dist/src/utils/auth/login.d.ts +1 -1
- package/dist/src/utils/auth/login.js +1 -1
- package/dist/src/utils/constants.d.ts +1 -1
- package/dist/src/utils/constants.js +1 -1
- package/dist/src/utils/version-checker.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -4
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zapier SDK CLI Login Package
|
|
3
|
+
*
|
|
4
|
+
* Handles login, logout, token cache and refresh for Zapier SDK CLI.
|
|
5
|
+
* Provides getToken function that can be optionally imported by zapier-sdk.
|
|
6
|
+
*/
|
|
7
|
+
import Conf from "conf";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import * as jwt from "jsonwebtoken";
|
|
10
|
+
import { getTokensFromKeychain, setTokensInKeychain, clearTokensFromKeychain, } from "./keychain";
|
|
11
|
+
export { createCache } from "./filesystem-cache";
|
|
12
|
+
/**
|
|
13
|
+
* Authentication error for token refresh failures.
|
|
14
|
+
* This is a standalone clone of ZapierAuthenticationError from the SDK.
|
|
15
|
+
* We can't import from SDK because cli-login must remain a standalone package
|
|
16
|
+
* with no SDK dependencies. The SDK recognizes this error by name.
|
|
17
|
+
*/
|
|
18
|
+
export class ZapierAuthenticationError extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "ZapierAuthenticationError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
let config = null;
|
|
25
|
+
// Default OAuth client ID
|
|
26
|
+
const DEFAULT_AUTH_CLIENT_ID = "grwWZD5hUWGvb4V8ODBuOtXer3h0DBEZ2HR8aay6";
|
|
27
|
+
// Buffer time before token expiration to trigger refresh (5 minutes)
|
|
28
|
+
const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
|
29
|
+
/**
|
|
30
|
+
* Creates a debug logger function.
|
|
31
|
+
* Returns a no-op function when debug is disabled.
|
|
32
|
+
*/
|
|
33
|
+
function createDebugLog(enabled) {
|
|
34
|
+
if (!enabled) {
|
|
35
|
+
return () => { };
|
|
36
|
+
}
|
|
37
|
+
return (message, data) => {
|
|
38
|
+
if (data === undefined) {
|
|
39
|
+
console.log(`[Zapier SDK CLI Login] ${message}`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(`[Zapier SDK CLI Login] ${message}`, data);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Censors sensitive values in headers for debug logging.
|
|
48
|
+
*/
|
|
49
|
+
function censorHeaderValue(value) {
|
|
50
|
+
if (value.length > 12) {
|
|
51
|
+
return `${value.substring(0, 4)}...${value.substring(value.length - 4)}`;
|
|
52
|
+
}
|
|
53
|
+
return `${value.charAt(0)}...`;
|
|
54
|
+
}
|
|
55
|
+
// Get auth client ID - uses passed value or falls back to default.
|
|
56
|
+
// Env var resolution is handled by the SDK before calling cli-login.
|
|
57
|
+
function getAuthClientId(clientId) {
|
|
58
|
+
return clientId || DEFAULT_AUTH_CLIENT_ID;
|
|
59
|
+
}
|
|
60
|
+
export const AUTH_MODE_HEADER = "X-Auth";
|
|
61
|
+
// Default auth base URL
|
|
62
|
+
const DEFAULT_AUTH_BASE_URL = "https://zapier.com";
|
|
63
|
+
/**
|
|
64
|
+
* Gets the OAuth token endpoint URL.
|
|
65
|
+
* baseUrl should be the auth base URL (e.g., https://zapier.com), already resolved by SDK.
|
|
66
|
+
*/
|
|
67
|
+
export function getAuthTokenUrl(options) {
|
|
68
|
+
const authBaseUrl = options?.baseUrl || DEFAULT_AUTH_BASE_URL;
|
|
69
|
+
return `${authBaseUrl}/oauth/token/`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Gets the OAuth authorization endpoint URL.
|
|
73
|
+
* baseUrl should be the auth base URL (e.g., https://zapier.com), already resolved by SDK.
|
|
74
|
+
*/
|
|
75
|
+
export function getAuthAuthorizeUrl(options) {
|
|
76
|
+
const authBaseUrl = options?.baseUrl || DEFAULT_AUTH_BASE_URL;
|
|
77
|
+
return `${authBaseUrl}/oauth/authorize/`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Gets all configuration needed for the PKCE login flow.
|
|
81
|
+
* Credentials should have baseUrl already resolved by SDK.
|
|
82
|
+
*/
|
|
83
|
+
export function getPkceLoginConfig(options) {
|
|
84
|
+
return {
|
|
85
|
+
clientId: getAuthClientId(options?.credentials?.clientId),
|
|
86
|
+
tokenUrl: getAuthTokenUrl({ baseUrl: options?.credentials?.baseUrl }),
|
|
87
|
+
authorizeUrl: getAuthAuthorizeUrl({
|
|
88
|
+
baseUrl: options?.credentials?.baseUrl,
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
let cachedLogin;
|
|
93
|
+
export function getConfig() {
|
|
94
|
+
if (!config) {
|
|
95
|
+
// Conf does not create the file until the first set() call.
|
|
96
|
+
config = new Conf({ projectName: "zapier-sdk-cli" });
|
|
97
|
+
// If the config file already exists but has no cache mode marker, an older
|
|
98
|
+
// SDK version created it. Set the marker to "config" so the login flow will
|
|
99
|
+
// prompt the user to upgrade to keychain cache. Otherwise, stamp "keychain"
|
|
100
|
+
// so that any future writes to the config don't leave it unmarked.
|
|
101
|
+
if (!config.has("login_storage_mode")) {
|
|
102
|
+
config.set("login_storage_mode", existsSync(config.path) ? "config" : "keychain");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return config;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Drops the cached Conf instance so the next getConfig() call re-initializes
|
|
109
|
+
* from disk (including re-running the pre-existing file detection).
|
|
110
|
+
*/
|
|
111
|
+
export function unloadConfig() {
|
|
112
|
+
config = null;
|
|
113
|
+
cachedLogin = undefined;
|
|
114
|
+
}
|
|
115
|
+
export async function updateLogin(loginData, options = {}) {
|
|
116
|
+
const debugLog = createDebugLog(options.debug ?? false);
|
|
117
|
+
const storage = options.storage ?? cachedLogin?.storage ?? "keychain";
|
|
118
|
+
const expiresAt = Date.now() + loginData.expires_in * 1000;
|
|
119
|
+
const cfg = getConfig();
|
|
120
|
+
cfg.set("login_storage_mode", storage);
|
|
121
|
+
if (storage === "keychain") {
|
|
122
|
+
await setTokensInKeychain({
|
|
123
|
+
data: {
|
|
124
|
+
login_jwt: loginData.access_token,
|
|
125
|
+
login_refresh_token: loginData.refresh_token,
|
|
126
|
+
},
|
|
127
|
+
debugLog,
|
|
128
|
+
});
|
|
129
|
+
cfg.set("login_expires_at", expiresAt);
|
|
130
|
+
cfg.delete("login_jwt");
|
|
131
|
+
cfg.delete("login_refresh_token");
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
cfg.set("login_jwt", loginData.access_token);
|
|
135
|
+
cfg.set("login_refresh_token", loginData.refresh_token);
|
|
136
|
+
cfg.set("login_expires_at", expiresAt);
|
|
137
|
+
await clearTokensFromKeychain({ debugLog });
|
|
138
|
+
}
|
|
139
|
+
cachedLogin = {
|
|
140
|
+
jwt: loginData.access_token,
|
|
141
|
+
refreshToken: loginData.refresh_token,
|
|
142
|
+
expiresAt,
|
|
143
|
+
storage,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// JWT utility functions
|
|
147
|
+
function decodeJwtOrThrow(token) {
|
|
148
|
+
if (typeof token !== "string") {
|
|
149
|
+
throw new Error("Expected JWT to be a string");
|
|
150
|
+
}
|
|
151
|
+
const decodedJwt = jwt.decode(token, { complete: true });
|
|
152
|
+
if (!decodedJwt) {
|
|
153
|
+
throw new Error("Could not decode JWT");
|
|
154
|
+
}
|
|
155
|
+
if (typeof decodedJwt.payload === "string") {
|
|
156
|
+
throw new Error("Did not expect JWT payload to be a string");
|
|
157
|
+
}
|
|
158
|
+
return decodedJwt;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Refreshes an expired JWT token using the refresh token.
|
|
162
|
+
* Returns the new access token or throws an error.
|
|
163
|
+
*/
|
|
164
|
+
async function refreshJwt(refreshToken, options = {}) {
|
|
165
|
+
const { onEvent, fetch = globalThis.fetch, credentials, debug = false, } = options;
|
|
166
|
+
const debugLog = createDebugLog(debug);
|
|
167
|
+
const tokenUrl = getAuthTokenUrl({ baseUrl: credentials?.baseUrl });
|
|
168
|
+
const clientId = getAuthClientId(credentials?.clientId);
|
|
169
|
+
const startTime = Date.now();
|
|
170
|
+
try {
|
|
171
|
+
onEvent?.({
|
|
172
|
+
type: "auth_refreshing",
|
|
173
|
+
payload: {
|
|
174
|
+
message: "Refreshing your token...",
|
|
175
|
+
operation: "token_refresh",
|
|
176
|
+
},
|
|
177
|
+
timestamp: Date.now(),
|
|
178
|
+
});
|
|
179
|
+
debugLog(`→ POST ${tokenUrl}`, {
|
|
180
|
+
headers: {
|
|
181
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
182
|
+
[AUTH_MODE_HEADER]: "no",
|
|
183
|
+
},
|
|
184
|
+
body: {
|
|
185
|
+
client_id: clientId,
|
|
186
|
+
refresh_token: censorHeaderValue(refreshToken),
|
|
187
|
+
grant_type: "refresh_token",
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const response = await fetch(tokenUrl, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: {
|
|
193
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
194
|
+
[AUTH_MODE_HEADER]: "no",
|
|
195
|
+
},
|
|
196
|
+
body: new URLSearchParams({
|
|
197
|
+
client_id: clientId,
|
|
198
|
+
refresh_token: refreshToken,
|
|
199
|
+
grant_type: "refresh_token",
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
const duration = Date.now() - startTime;
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
debugLog(`← ${response.status} ${response.statusText} (${duration}ms)`);
|
|
205
|
+
throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`);
|
|
206
|
+
}
|
|
207
|
+
debugLog(`← ${response.status} ${response.statusText} (${duration}ms)`);
|
|
208
|
+
const data = await response.json();
|
|
209
|
+
// Update stored login data
|
|
210
|
+
await updateLogin(data, { debug });
|
|
211
|
+
debugLog(`Token refreshed and saved to ${cachedLogin?.storage ?? "keychain"}`);
|
|
212
|
+
onEvent?.({
|
|
213
|
+
type: "auth_success",
|
|
214
|
+
payload: {
|
|
215
|
+
message: "Token refreshed successfully",
|
|
216
|
+
operation: "token_refresh",
|
|
217
|
+
},
|
|
218
|
+
timestamp: Date.now(),
|
|
219
|
+
});
|
|
220
|
+
return data.access_token;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
const duration = Date.now() - startTime;
|
|
224
|
+
debugLog(`✖ Token refresh failed (${duration}ms)`, {
|
|
225
|
+
error: error instanceof Error ? error.message : error,
|
|
226
|
+
});
|
|
227
|
+
// Clear in-memory cache so the next call re-reads from cache.
|
|
228
|
+
// Another process may have refreshed successfully in the meantime.
|
|
229
|
+
cachedLogin = undefined;
|
|
230
|
+
const errorMessage = `Token refresh failed: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
231
|
+
onEvent?.({
|
|
232
|
+
type: "auth_error",
|
|
233
|
+
payload: {
|
|
234
|
+
message: errorMessage,
|
|
235
|
+
error: errorMessage,
|
|
236
|
+
operation: "token_refresh",
|
|
237
|
+
},
|
|
238
|
+
timestamp: Date.now(),
|
|
239
|
+
});
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* In-flight refresh promise to prevent duplicate concurrent refreshes.
|
|
245
|
+
* Only one refresh can be in progress at a time within a single process.
|
|
246
|
+
*/
|
|
247
|
+
let pendingRefresh = null;
|
|
248
|
+
let pendingResolve = null;
|
|
249
|
+
/**
|
|
250
|
+
* Resolves stored login credentials from keychain or config.
|
|
251
|
+
* Caches the result in memory so the keychain is only read once per process.
|
|
252
|
+
* Deduplicates concurrent calls so only one keychain read is in flight at a time.
|
|
253
|
+
*/
|
|
254
|
+
async function resolveStoredLogin(debugLog) {
|
|
255
|
+
if (cachedLogin) {
|
|
256
|
+
debugLog("Using in-memory cached credentials");
|
|
257
|
+
return cachedLogin;
|
|
258
|
+
}
|
|
259
|
+
if (pendingResolve) {
|
|
260
|
+
debugLog("Waiting for existing keychain read to complete");
|
|
261
|
+
return pendingResolve;
|
|
262
|
+
}
|
|
263
|
+
pendingResolve = resolveStoredLoginFromStorage(debugLog).finally(() => {
|
|
264
|
+
pendingResolve = null;
|
|
265
|
+
});
|
|
266
|
+
return pendingResolve;
|
|
267
|
+
}
|
|
268
|
+
async function resolveStoredLoginFromStorage(debugLog) {
|
|
269
|
+
let cfg;
|
|
270
|
+
try {
|
|
271
|
+
cfg = getConfig();
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
debugLog("Failed to load config", {
|
|
275
|
+
error: error instanceof Error ? error.message : error,
|
|
276
|
+
});
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
const expiresAt = cfg.get("login_expires_at");
|
|
280
|
+
// Check for legacy config-only login (all three keys present)
|
|
281
|
+
const configJwt = cfg.get("login_jwt");
|
|
282
|
+
const configRefresh = cfg.get("login_refresh_token");
|
|
283
|
+
if (configJwt && configRefresh && typeof expiresAt === "number") {
|
|
284
|
+
debugLog("Loaded credentials from config (legacy format)");
|
|
285
|
+
cachedLogin = {
|
|
286
|
+
jwt: configJwt,
|
|
287
|
+
refreshToken: configRefresh,
|
|
288
|
+
expiresAt,
|
|
289
|
+
storage: "config",
|
|
290
|
+
};
|
|
291
|
+
return cachedLogin;
|
|
292
|
+
}
|
|
293
|
+
if (typeof expiresAt !== "number") {
|
|
294
|
+
debugLog("No stored login credentials found");
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
// expires_at exists without config tokens — read from keychain
|
|
298
|
+
const keychainData = await getTokensFromKeychain({ debugLog });
|
|
299
|
+
if (!keychainData) {
|
|
300
|
+
debugLog("No tokens found in keychain");
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
debugLog("Loaded credentials from keychain");
|
|
304
|
+
cachedLogin = {
|
|
305
|
+
jwt: keychainData.login_jwt,
|
|
306
|
+
refreshToken: keychainData.login_refresh_token,
|
|
307
|
+
expiresAt,
|
|
308
|
+
storage: "keychain",
|
|
309
|
+
};
|
|
310
|
+
return cachedLogin;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Resolves a valid token, refreshing if expired. Returns undefined if no
|
|
314
|
+
* stored login exists. Throws on refresh failure.
|
|
315
|
+
*/
|
|
316
|
+
async function resolveOrRefreshToken(options = {}) {
|
|
317
|
+
const { debug = false } = options;
|
|
318
|
+
const debugLog = createDebugLog(debug);
|
|
319
|
+
const stored = await resolveStoredLogin(debugLog);
|
|
320
|
+
if (!stored) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
const { jwt: storedJwt, refreshToken, expiresAt } = stored;
|
|
324
|
+
if (expiresAt > Date.now() + TOKEN_REFRESH_BUFFER_MS) {
|
|
325
|
+
debugLog("Using cached token (still valid)");
|
|
326
|
+
return storedJwt;
|
|
327
|
+
}
|
|
328
|
+
debugLog("Token expired, refreshing...");
|
|
329
|
+
if (pendingRefresh) {
|
|
330
|
+
debugLog("Waiting for existing refresh to complete");
|
|
331
|
+
return pendingRefresh;
|
|
332
|
+
}
|
|
333
|
+
pendingRefresh = refreshJwt(refreshToken, options).finally(() => {
|
|
334
|
+
pendingRefresh = null;
|
|
335
|
+
});
|
|
336
|
+
return await pendingRefresh;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Main function exported for zapier-sdk to optionally use.
|
|
340
|
+
* Returns the stored token from CLI configuration (with auto-refresh).
|
|
341
|
+
*
|
|
342
|
+
* Note: Environment variable handling (ZAPIER_CREDENTIALS, ZAPIER_TOKEN)
|
|
343
|
+
* is done by the SDK before calling this function.
|
|
344
|
+
*
|
|
345
|
+
* Returns undefined if no valid token is found.
|
|
346
|
+
*/
|
|
347
|
+
export async function getToken(options = {}) {
|
|
348
|
+
try {
|
|
349
|
+
return await resolveOrRefreshToken(options);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
const message = error instanceof Error ? error.message : "Token refresh failed";
|
|
353
|
+
throw new ZapierAuthenticationError(`${message}\nPlease run 'login' to authenticate again.`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Gets the logged-in user information from JWT token.
|
|
358
|
+
* Automatically refreshes token if expired.
|
|
359
|
+
*/
|
|
360
|
+
export async function getLoggedInUser(options = {}) {
|
|
361
|
+
const jwt = await getToken(options).catch(() => undefined);
|
|
362
|
+
if (!jwt) {
|
|
363
|
+
throw new Error("No valid authentication token available. Please login first.");
|
|
364
|
+
}
|
|
365
|
+
let decodedJwt = decodeJwtOrThrow(jwt);
|
|
366
|
+
if (decodedJwt.payload["sub_type"] == "service") {
|
|
367
|
+
decodedJwt = decodeJwtOrThrow(decodedJwt.payload["njwt"]);
|
|
368
|
+
}
|
|
369
|
+
if (typeof decodedJwt.payload["zap:acc"] !== "string") {
|
|
370
|
+
throw new Error("JWT payload does not contain accountId");
|
|
371
|
+
}
|
|
372
|
+
const accountId = parseInt(decodedJwt.payload["zap:acc"], 10);
|
|
373
|
+
if (isNaN(accountId)) {
|
|
374
|
+
throw new Error("JWT accountId is not a number");
|
|
375
|
+
}
|
|
376
|
+
if (decodedJwt.payload["sub_type"] !== "customuser" ||
|
|
377
|
+
typeof decodedJwt.payload["sub"] !== "string") {
|
|
378
|
+
throw new Error("JWT payload does not contain customUserId");
|
|
379
|
+
}
|
|
380
|
+
const customUserId = parseInt(decodedJwt.payload["sub"], 10);
|
|
381
|
+
if (isNaN(customUserId)) {
|
|
382
|
+
throw new Error("JWT customUserId is not a number");
|
|
383
|
+
}
|
|
384
|
+
const email = decodedJwt.payload["zap:uname"];
|
|
385
|
+
if (typeof email !== "string") {
|
|
386
|
+
throw new Error("JWT payload does not contain email");
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
accountId,
|
|
390
|
+
customUserId,
|
|
391
|
+
email,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Checks config to determine current cache mode without reading keychain.
|
|
396
|
+
* Credential keys in config are ground truth — if login_jwt exists, we're in
|
|
397
|
+
* config mode regardless of the marker. The marker only matters when no
|
|
398
|
+
* credential keys are present (e.g. after logout or when using keychain).
|
|
399
|
+
*/
|
|
400
|
+
export function getLoginStorageMode() {
|
|
401
|
+
const cfg = getConfig();
|
|
402
|
+
// Config tokens are ground truth — overrides any marker
|
|
403
|
+
if (typeof cfg.get("login_jwt") === "string") {
|
|
404
|
+
return "config";
|
|
405
|
+
}
|
|
406
|
+
// No config tokens — use the explicit marker (set by getConfig on init or
|
|
407
|
+
// by updateLogin on write)
|
|
408
|
+
const explicitMode = cfg.get("login_storage_mode");
|
|
409
|
+
if (explicitMode === "keychain" || explicitMode === "config") {
|
|
410
|
+
return explicitMode;
|
|
411
|
+
}
|
|
412
|
+
return "keychain";
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Clears stored login information.
|
|
416
|
+
*/
|
|
417
|
+
export async function logout(options = {}) {
|
|
418
|
+
const { onEvent } = options;
|
|
419
|
+
// Snapshot the cache mode before clearing tokens, so the marker survives
|
|
420
|
+
// logout. Without this, an old-SDK config login would lose its "config"
|
|
421
|
+
// signal once login_jwt is deleted.
|
|
422
|
+
const mode = getLoginStorageMode();
|
|
423
|
+
cachedLogin = undefined;
|
|
424
|
+
await clearTokensFromKeychain();
|
|
425
|
+
const cfg = getConfig();
|
|
426
|
+
cfg.set("login_storage_mode", mode);
|
|
427
|
+
cfg.delete("login_expires_at");
|
|
428
|
+
cfg.delete("login_jwt");
|
|
429
|
+
cfg.delete("login_refresh_token");
|
|
430
|
+
onEvent?.({
|
|
431
|
+
type: "auth_logout",
|
|
432
|
+
payload: { message: "Logged out successfully", operation: "logout" },
|
|
433
|
+
timestamp: Date.now(),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Gets the path to the configuration file.
|
|
438
|
+
*/
|
|
439
|
+
export function getConfigPath() {
|
|
440
|
+
const cfg = getConfig();
|
|
441
|
+
return cfg.path;
|
|
442
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface StoredTokens {
|
|
2
|
+
login_jwt: string;
|
|
3
|
+
login_refresh_token: string;
|
|
4
|
+
}
|
|
5
|
+
type DebugLog = (message: string, data?: unknown) => void;
|
|
6
|
+
export declare function getBackendInfo(): Promise<string>;
|
|
7
|
+
export declare function enqueue<T>(fn: () => Promise<T>): Promise<T>;
|
|
8
|
+
export declare function getTokensFromKeychain({ debugLog, }?: {
|
|
9
|
+
debugLog?: DebugLog;
|
|
10
|
+
}): Promise<StoredTokens | undefined>;
|
|
11
|
+
export declare function setTokensInKeychain({ data, debugLog, }: {
|
|
12
|
+
data: StoredTokens;
|
|
13
|
+
debugLog?: DebugLog;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
export declare function clearTokensFromKeychain({ debugLog, }?: {
|
|
16
|
+
debugLog?: DebugLog;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { getPassword, setPassword, deletePassword, getKeyring, } from "cross-keychain";
|
|
2
|
+
const SERVICE = "zapier-sdk-cli";
|
|
3
|
+
const ACCOUNT = "login";
|
|
4
|
+
let cachedBackendInfo;
|
|
5
|
+
export async function getBackendInfo() {
|
|
6
|
+
if (!cachedBackendInfo) {
|
|
7
|
+
const keyring = await getKeyring();
|
|
8
|
+
cachedBackendInfo = `${keyring.name} (${keyring.id})`;
|
|
9
|
+
}
|
|
10
|
+
return cachedBackendInfo;
|
|
11
|
+
}
|
|
12
|
+
// Serialize all keychain operations so concurrent reads and writes never
|
|
13
|
+
// overlap. Mostly, this is to ensure we don't try to read user info while
|
|
14
|
+
// in the middle of a logout. We don't really need to make reads serial, but
|
|
15
|
+
// we cache those elsewhere anyway, so we use a simple serial queue.
|
|
16
|
+
let keychainQueue = Promise.resolve();
|
|
17
|
+
export function enqueue(fn) {
|
|
18
|
+
// fn is passed as both fulfillment and rejection handler so a failed
|
|
19
|
+
// operation never blocks subsequent ones from running.
|
|
20
|
+
const result = keychainQueue.then(fn, fn);
|
|
21
|
+
keychainQueue = result.then(() => { }, () => { });
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
export async function getTokensFromKeychain({ debugLog, } = {}) {
|
|
25
|
+
return enqueue(async () => {
|
|
26
|
+
const backendInfo = await getBackendInfo();
|
|
27
|
+
debugLog?.(`Keychain read via ${backendInfo}`);
|
|
28
|
+
const startTime = Date.now();
|
|
29
|
+
const raw = await getPassword(SERVICE, ACCOUNT);
|
|
30
|
+
debugLog?.(`Keychain read completed in ${Date.now() - startTime}ms`);
|
|
31
|
+
if (!raw) {
|
|
32
|
+
debugLog?.("Keychain returned no data");
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
debugLog?.("Keychain data is not valid JSON");
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
if (typeof parsed.login_jwt === "string" &&
|
|
44
|
+
typeof parsed.login_refresh_token === "string") {
|
|
45
|
+
return {
|
|
46
|
+
login_jwt: parsed.login_jwt,
|
|
47
|
+
login_refresh_token: parsed.login_refresh_token,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
debugLog?.("Keychain data has invalid shape", parsed);
|
|
51
|
+
return undefined;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export async function setTokensInKeychain({ data, debugLog, }) {
|
|
55
|
+
return enqueue(async () => {
|
|
56
|
+
const backendInfo = await getBackendInfo();
|
|
57
|
+
debugLog?.(`Keychain write via ${backendInfo}`);
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
await setPassword(SERVICE, ACCOUNT, JSON.stringify(data));
|
|
60
|
+
debugLog?.(`Keychain write completed in ${Date.now() - startTime}ms`);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export async function clearTokensFromKeychain({ debugLog, } = {}) {
|
|
64
|
+
return enqueue(async () => {
|
|
65
|
+
try {
|
|
66
|
+
const backendInfo = await getBackendInfo();
|
|
67
|
+
debugLog?.(`Keychain clear via ${backendInfo}`);
|
|
68
|
+
await deletePassword(SERVICE, ACCOUNT);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Best-effort: keychain may not have an entry or may be unavailable
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
package/dist/src/login.d.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Public entrypoint for CLI login + cache functionality.
|
|
3
|
+
*
|
|
4
|
+
* The SDK dynamically imports from here (via the `./login` export map)
|
|
5
|
+
* to find PKCE `getToken` and the default `createCache` provider.
|
|
6
|
+
* This package also hosts the implementation directly — there is no
|
|
7
|
+
* longer a separate @zapier/zapier-sdk-cli-login package.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./login/index";
|
|
10
|
+
export { createCache } from "./login/filesystem-cache";
|
package/dist/src/login.js
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Public entrypoint for CLI login + cache functionality.
|
|
3
|
+
*
|
|
4
|
+
* The SDK dynamically imports from here (via the `./login` export map)
|
|
5
|
+
* to find PKCE `getToken` and the default `createCache` provider.
|
|
6
|
+
* This package also hosts the implementation directly — there is no
|
|
7
|
+
* longer a separate @zapier/zapier-sdk-cli-login package.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./login/index";
|
|
10
|
+
export { createCache } from "./login/filesystem-cache";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FeedbackSchema } from "./schemas";
|
|
2
2
|
import { createFunction } from "@zapier/zapier-sdk";
|
|
3
|
-
import { getLoggedInUser } from "
|
|
3
|
+
import { getLoggedInUser } from "../../login";
|
|
4
4
|
const feedbackResolver = {
|
|
5
5
|
type: "static",
|
|
6
6
|
inputType: "text",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GetLoginConfigPathSchema, } from "./schemas";
|
|
2
2
|
import { createFunction } from "@zapier/zapier-sdk";
|
|
3
|
-
import { getConfigPath } from "
|
|
3
|
+
import { getConfigPath } from "../../login";
|
|
4
4
|
export const getLoginConfigPathPlugin = () => {
|
|
5
5
|
const getLoginConfigPathWithSdk = createFunction(async function getLoginConfigPathWithSdk(_options) {
|
|
6
6
|
return getConfigPath();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isCredentialsObject, buildApplicationLifecycleEvent, } from "@zapier/zapier-sdk";
|
|
2
2
|
import login from "../../utils/auth/login";
|
|
3
|
-
import { getLoggedInUser
|
|
3
|
+
import { getLoggedInUser } from "../../login";
|
|
4
4
|
import { LoginSchema } from "./schemas";
|
|
5
5
|
function toPkceCredentials(credentials) {
|
|
6
6
|
if (credentials &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createFunction } from "@zapier/zapier-sdk";
|
|
2
|
-
import { logout } from "
|
|
2
|
+
import { logout } from "../../login";
|
|
3
3
|
import { LogoutSchema } from "./schemas";
|
|
4
4
|
const logoutWithSdk = createFunction(async function logoutWithSdk(_options) {
|
|
5
5
|
await logout();
|
package/dist/src/sdk.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as cliLogin from "
|
|
1
|
+
import * as cliLogin from "./login";
|
|
2
2
|
import { createZapierSdk, injectCliLogin, } from "@zapier/zapier-sdk";
|
|
3
3
|
import { loginPlugin, logoutPlugin, mcpPlugin, bundleCodePlugin, getLoginConfigPathPlugin, addPlugin, generateAppTypesPlugin, buildManifestPlugin, feedbackPlugin, curlPlugin, cliOverridesPlugin, initPlugin, } from "./plugins/index";
|
|
4
4
|
import packageJson from "../package.json" with { type: "json" };
|
|
@@ -8,7 +8,7 @@ import log from "../log";
|
|
|
8
8
|
import api from "../api/client";
|
|
9
9
|
import getCallablePromise from "../getCallablePromise";
|
|
10
10
|
import inquirer from "inquirer";
|
|
11
|
-
import { updateLogin, logout, getPkceLoginConfig, getLoginStorageMode, getConfigPath, } from "
|
|
11
|
+
import { updateLogin, logout, getPkceLoginConfig, getLoginStorageMode, getConfigPath, } from "../../login";
|
|
12
12
|
import { ZapierCliUserCancellationError } from "../errors";
|
|
13
13
|
const findAvailablePort = () => {
|
|
14
14
|
return new Promise((resolve, reject) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Import shared OAuth constants from login package
|
|
2
|
-
export { AUTH_MODE_HEADER } from "
|
|
2
|
+
export { AUTH_MODE_HEADER } from "../login";
|
|
3
3
|
// CLI-specific constants
|
|
4
4
|
export const LOGIN_PORTS = [49505, 50575, 52804, 55981, 61010, 63851];
|
|
5
5
|
export const LOGIN_TIMEOUT_MS = 300000; // 5 minutes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import packageJsonLib from "package-json";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import log from "./log";
|
|
4
|
-
import { getConfig } from "
|
|
4
|
+
import { getConfig } from "../login";
|
|
5
5
|
import { getUpdateCommand } from "./package-manager-detector";
|
|
6
6
|
import semver from "semver";
|
|
7
7
|
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|