@zapier/zapier-sdk-cli 0.61.3 → 0.63.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.
@@ -1,5 +1,5 @@
1
1
  import * as jwt from 'jsonwebtoken';
2
- import { deletePassword, setPassword, getKeyring, getPassword } from 'cross-keychain';
2
+ import { deletePassword, getKeyring, setPassword, getPassword } from 'cross-keychain';
3
3
  import Conf from 'conf';
4
4
  import * as fs from 'fs';
5
5
  import { promises, createWriteStream, existsSync, readdirSync, rmSync, mkdirSync, writeFileSync, copyFileSync, readFileSync } from 'fs';
@@ -7,7 +7,7 @@ import crypto, { createHash } from 'crypto';
7
7
  import * as path from 'path';
8
8
  import { resolve, join, dirname, basename, relative, extname } from 'path';
9
9
  import * as lockfile from 'proper-lockfile';
10
- import { definePlugin, createPluginMethod, OutputPropertySchema, ZapierBundleError, DEFAULT_CONFIG_PATH, ZapierValidationError, ZapierUnknownError, ZapierReleaseTriggerMessageSignal, getOrCreateApiClient, isPermanentHttpError, invalidateCachedToken, batch, toSnakeCase, ZapierAbortDrainSignal, isCredentialsObject, buildApplicationLifecycleEvent, AuthMechanism, ZapierAuthenticationError, ZapierError, DEPRECATION_NOTICE_EVENT } from '@zapier/zapier-sdk';
10
+ import { definePlugin, createPluginMethod, OutputPropertySchema, ZapierBundleError, DEFAULT_CONFIG_PATH, ZapierValidationError, ZapierUnknownError, ZapierReleaseTriggerMessageSignal, isPermanentHttpError, invalidateCachedToken, batch, toSnakeCase, ZapierAbortDrainSignal, getOrCreateApiClient, isCredentialsObject, ZapierAuthenticationError, ZapierError, buildApplicationLifecycleEvent, AuthMechanism, DEPRECATION_NOTICE_EVENT } from '@zapier/zapier-sdk';
11
11
  import { z } from 'zod';
12
12
  import { injectCliLogin, createZapierSdkStack, createSdk, defineLegacyMerge, zapierSdkPlugin, addPlugin } from '@zapier/zapier-sdk/experimental';
13
13
  import { hostname } from 'os';
@@ -955,6 +955,39 @@ async function resolveCredentialsBaseUrl(context) {
955
955
  return getBaseUrlFromResolvedCredentials(resolvedCredentials) ?? getBaseUrlFromOptionsCredentials(context.options?.credentials) ?? context.options?.baseUrl;
956
956
  }
957
957
 
958
+ // src/utils/cli-argument-sanitizer.ts
959
+ var SENSITIVE_FLAGS = [
960
+ "--credentials",
961
+ "--credentials-client-secret",
962
+ "--credentials-client-id",
963
+ "--credentials-base-url",
964
+ "--callback-url",
965
+ "--user",
966
+ "--header",
967
+ "-H",
968
+ "-u"
969
+ ];
970
+ function sanitizeCliArguments(args) {
971
+ const sanitized = [];
972
+ let skipNext = false;
973
+ for (const arg of args) {
974
+ if (skipNext) {
975
+ skipNext = false;
976
+ sanitized.push("[REDACTED]");
977
+ continue;
978
+ }
979
+ if (SENSITIVE_FLAGS.some((flag) => arg.startsWith(flag + "="))) {
980
+ sanitized.push(arg.split("=")[0] + "=[REDACTED]");
981
+ } else if (SENSITIVE_FLAGS.includes(arg)) {
982
+ sanitized.push(arg);
983
+ skipNext = true;
984
+ } else {
985
+ sanitized.push(arg);
986
+ }
987
+ }
988
+ return sanitized;
989
+ }
990
+
958
991
  // src/utils/non-interactive.ts
959
992
  function resolveNonInteractive(options) {
960
993
  return options.nonInteractive === true || options.skipPrompts === true || !process.stdin.isTTY || !process.stdout.isTTY;
@@ -1274,6 +1307,14 @@ var client_default = api;
1274
1307
 
1275
1308
  // src/utils/auth/oauth-transaction.ts
1276
1309
  var OAUTH_LOOPBACK_HOST = "localhost";
1310
+ var OauthTransactionSchema = z.object({
1311
+ browserAuthUrl: z.string(),
1312
+ clientId: z.string(),
1313
+ codeVerifier: z.string(),
1314
+ redirectUri: z.string(),
1315
+ state: z.string(),
1316
+ tokenUrl: z.string()
1317
+ });
1277
1318
  function buildBrowserAuthUrl({
1278
1319
  authorizeUrl,
1279
1320
  entryPoint = "login"
@@ -1362,8 +1403,51 @@ async function exchangeOauthCode({
1362
1403
  expiresIn: data.expires_in
1363
1404
  };
1364
1405
  }
1406
+ var PENDING_OAUTH_TRANSACTION_KEY = "pending_oauth_transaction";
1407
+ var PendingOauthTransactionSchema = z.object({
1408
+ version: z.literal(1),
1409
+ entryPoint: z.enum(["login", "signup"]),
1410
+ expiresAt: z.number(),
1411
+ credentialsBaseUrl: z.string().optional(),
1412
+ credentialName: z.string().optional(),
1413
+ useApprovals: z.boolean(),
1414
+ headless: z.boolean().default(false),
1415
+ transaction: OauthTransactionSchema
1416
+ });
1417
+ function savePendingOauthTransaction({
1418
+ entryPoint,
1419
+ transaction,
1420
+ expiresAt,
1421
+ credentialsBaseUrl,
1422
+ credentialName,
1423
+ useApprovals,
1424
+ headless
1425
+ }) {
1426
+ const pendingTransaction = {
1427
+ version: 1,
1428
+ entryPoint,
1429
+ expiresAt,
1430
+ useApprovals,
1431
+ headless,
1432
+ transaction,
1433
+ ...credentialsBaseUrl !== void 0 && { credentialsBaseUrl },
1434
+ ...credentialName !== void 0 && { credentialName }
1435
+ };
1436
+ getConfig().set(PENDING_OAUTH_TRANSACTION_KEY, pendingTransaction);
1437
+ return pendingTransaction;
1438
+ }
1439
+ function getPendingOauthTransaction() {
1440
+ const result = PendingOauthTransactionSchema.safeParse(
1441
+ getConfig().get(PENDING_OAUTH_TRANSACTION_KEY)
1442
+ );
1443
+ return result.success ? result.data : void 0;
1444
+ }
1445
+ function clearPendingOauthTransaction() {
1446
+ getConfig().delete(PENDING_OAUTH_TRANSACTION_KEY);
1447
+ }
1365
1448
 
1366
1449
  // src/utils/auth/oauth-flow.ts
1450
+ var PENDING_OAUTH_RECOVERY_MESSAGE = "Start a new non-interactive login or signup flow to generate a fresh URL.";
1367
1451
  var LOGIN_OAUTH_COPY = {
1368
1452
  flowName: "Login",
1369
1453
  action: "log in",
@@ -1419,6 +1503,95 @@ async function runSignupOauthFlow(options) {
1419
1503
  copy: SIGNUP_OAUTH_COPY
1420
1504
  });
1421
1505
  }
1506
+ async function startPendingOauthFlow({
1507
+ timeoutMs = LOGIN_TIMEOUT_MS,
1508
+ pkceCredentials,
1509
+ baseUrl,
1510
+ entryPoint,
1511
+ credentialName,
1512
+ useApprovals,
1513
+ headless
1514
+ }) {
1515
+ const copy = entryPoint === "signup" ? SIGNUP_OAUTH_COPY : LOGIN_OAUTH_COPY;
1516
+ const port = LOGIN_PORTS[0];
1517
+ const transaction = await prepareOauthTransaction({
1518
+ pkceCredentials,
1519
+ baseUrl,
1520
+ redirectUri: `http://${OAUTH_LOOPBACK_HOST}:${port}/oauth`,
1521
+ entryPoint
1522
+ });
1523
+ const pending = savePendingOauthTransaction({
1524
+ entryPoint,
1525
+ transaction,
1526
+ expiresAt: Date.now() + timeoutMs,
1527
+ credentialsBaseUrl: baseUrl,
1528
+ credentialName,
1529
+ useApprovals,
1530
+ headless
1531
+ });
1532
+ process.stderr.write(
1533
+ `Open this ${copy.urlLabel} in a browser to continue:
1534
+ `
1535
+ );
1536
+ console.log(transaction.browserAuthUrl);
1537
+ if (!headless) {
1538
+ process.stderr.write(`Opening your browser to ${entryPoint}.
1539
+ `);
1540
+ try {
1541
+ await open(transaction.browserAuthUrl);
1542
+ } catch (error) {
1543
+ const reason = error instanceof Error ? error.message : String(error);
1544
+ process.stderr.write(
1545
+ `Browser did not open automatically to ${entryPoint}: ${reason}
1546
+ `
1547
+ );
1548
+ }
1549
+ }
1550
+ process.stderr.write(
1551
+ `After authorizing, finish with \`zapier-sdk ${entryPoint} --callback-url <final-url>\`.
1552
+ `
1553
+ );
1554
+ return pending;
1555
+ }
1556
+ function getPendingOauthFlow({
1557
+ entryPoint
1558
+ }) {
1559
+ const pending = getPendingOauthTransaction();
1560
+ if (!pending) {
1561
+ throw new ZapierCliValidationError(
1562
+ `No pending ${entryPoint} OAuth flow found. ${PENDING_OAUTH_RECOVERY_MESSAGE}`
1563
+ );
1564
+ }
1565
+ if (Date.now() > pending.expiresAt) {
1566
+ clearPendingOauthTransaction();
1567
+ throw new ZapierCliValidationError(
1568
+ `Pending ${pending.entryPoint} OAuth flow has expired. ${PENDING_OAUTH_RECOVERY_MESSAGE}`
1569
+ );
1570
+ }
1571
+ if (pending.entryPoint !== entryPoint) {
1572
+ throw new ZapierCliValidationError(
1573
+ `Pending OAuth flow is for ${pending.entryPoint}, not ${entryPoint}. ${PENDING_OAUTH_RECOVERY_MESSAGE}`
1574
+ );
1575
+ }
1576
+ return pending;
1577
+ }
1578
+ async function resumePendingOauthFlow({
1579
+ pending,
1580
+ callbackUrl,
1581
+ onProgress
1582
+ }) {
1583
+ const code = getCallbackCode({
1584
+ callbackUrl,
1585
+ transaction: pending.transaction
1586
+ });
1587
+ onProgress?.({ type: "callback_accepted" });
1588
+ process.stderr.write("Exchanging authorization code for tokens...\n");
1589
+ onProgress?.({ type: "token_exchange_started" });
1590
+ const tokens = await exchangeOauthCode({ ...pending.transaction, code });
1591
+ clearPendingOauthTransaction();
1592
+ onProgress?.({ type: "token_exchange_completed" });
1593
+ return tokens;
1594
+ }
1422
1595
  function runOauthFlowForEntryPoint({
1423
1596
  options,
1424
1597
  entryPoint,
@@ -1954,12 +2127,19 @@ async function saveClientCredentials({
1954
2127
  function emitAccountAuthSuccess({
1955
2128
  sdk,
1956
2129
  profile,
1957
- clientId
2130
+ clientId,
2131
+ isNonInteractive,
2132
+ isHeadless
1958
2133
  }) {
1959
2134
  sdk.context.eventEmission.emit(
1960
2135
  "platform.sdk.ApplicationLifecycleEvent",
1961
2136
  buildApplicationLifecycleEvent(
1962
- { lifecycle_event_type: "login_success" },
2137
+ {
2138
+ lifecycle_event_type: "login_success",
2139
+ used_non_interactive_mode: isNonInteractive,
2140
+ is_headless: isHeadless,
2141
+ process_argv: sanitizeCliArguments(process.argv.slice(2))
2142
+ },
1963
2143
  {
1964
2144
  customuser_id: profile.user_id,
1965
2145
  account_id: profile.roles[0]?.account_id ?? null,
@@ -1970,11 +2150,18 @@ function emitAccountAuthSuccess({
1970
2150
  );
1971
2151
  }
1972
2152
  function emitSignupSuccess({
1973
- sdk
2153
+ sdk,
2154
+ isNonInteractive,
2155
+ isHeadless
1974
2156
  }) {
1975
2157
  sdk.context.eventEmission.emit(
1976
2158
  "platform.sdk.ApplicationLifecycleEvent",
1977
- buildApplicationLifecycleEvent({ lifecycle_event_type: "signup_success" })
2159
+ buildApplicationLifecycleEvent({
2160
+ lifecycle_event_type: "signup_success",
2161
+ used_non_interactive_mode: isNonInteractive,
2162
+ is_headless: isHeadless,
2163
+ process_argv: sanitizeCliArguments(process.argv.slice(2))
2164
+ })
1978
2165
  );
1979
2166
  }
1980
2167
  async function runOauthWithRedaction(runOauth) {
@@ -2005,7 +2192,11 @@ async function runOauthForEntryPoint({
2005
2192
  recoveryMessage: headless ? HEADLESS_SIGNUP_RECOVERY_MESSAGE : SIGNUP_RECOVERY_MESSAGE,
2006
2193
  onProgress: (event) => {
2007
2194
  if (event.type === "callback_accepted") {
2008
- emitSignupSuccess({ sdk });
2195
+ emitSignupSuccess({
2196
+ sdk,
2197
+ isNonInteractive: false,
2198
+ isHeadless: headless === true
2199
+ });
2009
2200
  }
2010
2201
  }
2011
2202
  })
@@ -2022,11 +2213,124 @@ async function runOauthForEntryPoint({
2022
2213
  })
2023
2214
  );
2024
2215
  }
2216
+ async function provisionAccountCredentials({
2217
+ sdk,
2218
+ entryPoint,
2219
+ accessToken,
2220
+ credentialsBaseUrl,
2221
+ credentialName,
2222
+ interactive,
2223
+ useApprovals,
2224
+ isNonInteractive,
2225
+ isHeadless
2226
+ }) {
2227
+ const scopedApi = getOrCreateApiClient({
2228
+ credentials: accessToken,
2229
+ baseUrl: credentialsBaseUrl,
2230
+ callerPackage: sdk.context.options?.callerPackage
2231
+ });
2232
+ const profile = await getProfile(scopedApi);
2233
+ process.stderr.write(`${getProfileMessage(entryPoint, profile.email)}
2234
+ `);
2235
+ process.stderr.write(
2236
+ "\nGenerating credentials so this machine can make authenticated requests on your behalf.\n"
2237
+ );
2238
+ const resolvedCredentialName = credentialName ?? await resolveCredentialName({
2239
+ email: profile.email,
2240
+ interactive,
2241
+ baseUrl: credentialsBaseUrl,
2242
+ entryPoint
2243
+ });
2244
+ const { clientId } = await saveClientCredentials({
2245
+ api: scopedApi,
2246
+ name: resolvedCredentialName,
2247
+ credentialsBaseUrl,
2248
+ useApprovals,
2249
+ cleanupLogPrefix: entryPoint
2250
+ });
2251
+ process.stderr.write(
2252
+ `\u2705 Credentials "${resolvedCredentialName}" created and set as default. You are ready to use the Zapier SDK.
2253
+ `
2254
+ );
2255
+ if (useApprovals) {
2256
+ process.stderr.write("\u{1F510} Approvals are enabled for these credentials.\n");
2257
+ }
2258
+ emitAccountAuthSuccess({
2259
+ sdk,
2260
+ profile,
2261
+ clientId,
2262
+ isNonInteractive,
2263
+ isHeadless
2264
+ });
2265
+ }
2025
2266
  async function runAccountAuth({
2026
2267
  sdk,
2027
2268
  options,
2028
2269
  entryPoint
2029
2270
  }) {
2271
+ if (options.callbackUrl !== void 0) {
2272
+ const { callbackUrl } = options;
2273
+ const pending = getPendingOauthFlow({ entryPoint });
2274
+ const finishName = options.name !== void 0 ? validateCredentialsName(options.name) : void 0;
2275
+ if (finishName !== void 0 && finishName !== pending.credentialName) {
2276
+ throw new ZapierCliValidationError(
2277
+ "Cannot change --name while completing a pending OAuth flow. Start a new non-interactive login with the desired --name."
2278
+ );
2279
+ }
2280
+ if (options.useApprovals === true && !pending.useApprovals) {
2281
+ throw new ZapierCliValidationError(
2282
+ "Cannot add --use-approvals while completing a pending OAuth flow. Start a new non-interactive flow with --use-approvals."
2283
+ );
2284
+ }
2285
+ if (pending.credentialName !== void 0) {
2286
+ const activeCredentials = getActiveCredentials({
2287
+ baseUrl: pending.credentialsBaseUrl
2288
+ });
2289
+ if (activeCredentials?.name !== pending.credentialName && credentialNameExists({
2290
+ name: pending.credentialName,
2291
+ baseUrl: pending.credentialsBaseUrl
2292
+ })) {
2293
+ throw new ZapierCliValidationError(
2294
+ `A credential named "${pending.credentialName}" already exists. Choose a different --name.`
2295
+ );
2296
+ }
2297
+ }
2298
+ if (!await clearExistingAuthState({
2299
+ sdk,
2300
+ baseUrl: pending.credentialsBaseUrl,
2301
+ interactive: false,
2302
+ entryPoint
2303
+ })) {
2304
+ return;
2305
+ }
2306
+ const { accessToken: accessToken2 } = await runOauthWithRedaction(async () => {
2307
+ return resumePendingOauthFlow({
2308
+ callbackUrl,
2309
+ pending,
2310
+ onProgress: (event) => {
2311
+ if (entryPoint === "signup" && event.type === "callback_accepted") {
2312
+ emitSignupSuccess({
2313
+ sdk,
2314
+ isNonInteractive: true,
2315
+ isHeadless: pending.headless
2316
+ });
2317
+ }
2318
+ }
2319
+ });
2320
+ });
2321
+ await provisionAccountCredentials({
2322
+ sdk,
2323
+ entryPoint,
2324
+ accessToken: accessToken2,
2325
+ credentialsBaseUrl: pending.credentialsBaseUrl,
2326
+ credentialName: pending.credentialName,
2327
+ interactive: false,
2328
+ useApprovals: pending.useApprovals,
2329
+ isNonInteractive: true,
2330
+ isHeadless: pending.headless
2331
+ });
2332
+ return;
2333
+ }
2030
2334
  const timeoutSeconds = parseTimeoutSeconds(options.timeout);
2031
2335
  const interactive = !resolveNonInteractive(options);
2032
2336
  const resolvedCredentials = await sdk.context.resolveCredentials();
@@ -2055,6 +2359,19 @@ async function runAccountAuth({
2055
2359
  })) {
2056
2360
  return;
2057
2361
  }
2362
+ const useApprovals = options.useApprovals === true;
2363
+ if (!interactive) {
2364
+ await startPendingOauthFlow({
2365
+ entryPoint,
2366
+ timeoutMs: timeoutSeconds * 1e3,
2367
+ pkceCredentials,
2368
+ baseUrl: credentialsBaseUrl,
2369
+ credentialName: providedName,
2370
+ useApprovals,
2371
+ headless
2372
+ });
2373
+ return;
2374
+ }
2058
2375
  const { accessToken } = await runOauthForEntryPoint({
2059
2376
  sdk,
2060
2377
  entryPoint,
@@ -2064,39 +2381,17 @@ async function runAccountAuth({
2064
2381
  headless,
2065
2382
  interactive
2066
2383
  });
2067
- const scopedApi = getOrCreateApiClient({
2068
- credentials: accessToken,
2069
- baseUrl: credentialsBaseUrl,
2070
- callerPackage: sdk.context.options?.callerPackage
2071
- });
2072
- const profile = await getProfile(scopedApi);
2073
- process.stderr.write(`${getProfileMessage(entryPoint, profile.email)}
2074
- `);
2075
- process.stderr.write(
2076
- "\nGenerating credentials so this machine can make authenticated requests on your behalf.\n"
2077
- );
2078
- const credentialName = providedName ?? await resolveCredentialName({
2079
- email: profile.email,
2080
- interactive,
2081
- baseUrl: credentialsBaseUrl,
2082
- entryPoint
2083
- });
2084
- const useApprovals = options.useApprovals === true;
2085
- const { clientId } = await saveClientCredentials({
2086
- api: scopedApi,
2087
- name: credentialName,
2384
+ await provisionAccountCredentials({
2385
+ sdk,
2386
+ entryPoint,
2387
+ accessToken,
2088
2388
  credentialsBaseUrl,
2389
+ credentialName: providedName,
2390
+ interactive,
2089
2391
  useApprovals,
2090
- cleanupLogPrefix: entryPoint
2392
+ isNonInteractive: false,
2393
+ isHeadless: headless
2091
2394
  });
2092
- process.stderr.write(
2093
- `\u2705 Credentials "${credentialName}" created and set as default. You are ready to use the Zapier SDK.
2094
- `
2095
- );
2096
- if (useApprovals) {
2097
- process.stderr.write("\u{1F510} Approvals are enabled for these credentials.\n");
2098
- }
2099
- emitAccountAuthSuccess({ sdk, profile, clientId });
2100
2395
  }
2101
2396
  var LoginSchema = z.object({
2102
2397
  name: z.string().optional().describe(
@@ -2116,8 +2411,20 @@ var LoginSchema = z.object({
2116
2411
  }),
2117
2412
  headless: z.boolean().optional().describe(
2118
2413
  "Use when logging in from a machine that has no browser. Prints a login link to open elsewhere, then accepts the pasted loopback callback URL."
2414
+ ),
2415
+ callbackUrl: z.string().optional().describe(
2416
+ "Resume a pending non-interactive login with the final OAuth callback URL from your browser."
2119
2417
  )
2120
- }).describe("Log in to Zapier to access your account");
2418
+ }).describe("Log in to Zapier to access your account").meta({
2419
+ cliTelemetry: {
2420
+ authFlow: {
2421
+ callbackUrlParam: "callbackUrl",
2422
+ headlessParam: "headless",
2423
+ nonInteractiveParam: "nonInteractive",
2424
+ skipPromptsParam: "skipPrompts"
2425
+ }
2426
+ }
2427
+ });
2121
2428
 
2122
2429
  // src/plugins/login/index.ts
2123
2430
  var loginPlugin = definePlugin(
@@ -2146,8 +2453,20 @@ var SignupSchema = z.object({
2146
2453
  }),
2147
2454
  headless: z.boolean().optional().describe(
2148
2455
  "Use when signing up from a machine that has no browser. Prints a signup link to open elsewhere, then accepts the pasted loopback callback URL."
2456
+ ),
2457
+ callbackUrl: z.string().optional().describe(
2458
+ "Resume a pending non-interactive signup with the final OAuth callback URL from your browser."
2149
2459
  )
2150
- }).describe("Set up Zapier account access and SDK credentials");
2460
+ }).describe("Set up Zapier account access and SDK credentials").meta({
2461
+ cliTelemetry: {
2462
+ authFlow: {
2463
+ callbackUrlParam: "callbackUrl",
2464
+ headlessParam: "headless",
2465
+ nonInteractiveParam: "nonInteractive",
2466
+ skipPromptsParam: "skipPrompts"
2467
+ }
2468
+ }
2469
+ });
2151
2470
 
2152
2471
  // src/plugins/signup/index.ts
2153
2472
  var signupPlugin = definePlugin(
@@ -4705,7 +5024,7 @@ function collectDeprecationNoticeAndForward(event, onEvent) {
4705
5024
  // package.json with { type: 'json' }
4706
5025
  var package_default = {
4707
5026
  name: "@zapier/zapier-sdk-cli",
4708
- version: "0.61.3"};
5027
+ version: "0.63.0"};
4709
5028
 
4710
5029
  // src/experimental.ts
4711
5030
  injectCliLogin(login_exports);
@@ -543,6 +543,7 @@ declare const signupPlugin: (sdk: {
543
543
  nonInteractive?: boolean | undefined;
544
544
  skipPrompts?: boolean | undefined;
545
545
  headless?: boolean | undefined;
546
+ callbackUrl?: string | undefined;
546
547
  } | undefined) => Promise<void>;
547
548
  } & {
548
549
  context: {
@@ -543,6 +543,7 @@ declare const signupPlugin: (sdk: {
543
543
  nonInteractive?: boolean | undefined;
544
544
  skipPrompts?: boolean | undefined;
545
545
  headless?: boolean | undefined;
546
+ callbackUrl?: string | undefined;
546
547
  } | undefined) => Promise<void>;
547
548
  } & {
548
549
  context: {