@zapier/zapier-sdk-cli 0.47.0 → 0.48.1

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +2 -1
  3. package/dist/cli.cjs +581 -86
  4. package/dist/cli.mjs +581 -86
  5. package/dist/experimental.cjs +562 -86
  6. package/dist/experimental.mjs +561 -85
  7. package/dist/index.cjs +563 -87
  8. package/dist/index.mjs +562 -86
  9. package/dist/login.cjs +94 -25
  10. package/dist/login.d.mts +29 -2
  11. package/dist/login.d.ts +29 -2
  12. package/dist/login.mjs +90 -25
  13. package/dist/package.json +1 -1
  14. package/dist/src/cli.js +32 -3
  15. package/dist/src/login/config.d.ts +4 -0
  16. package/dist/src/login/config.js +21 -0
  17. package/dist/src/login/credentials-revoke.d.ts +13 -0
  18. package/dist/src/login/credentials-revoke.js +48 -0
  19. package/dist/src/login/credentials-store.d.ts +33 -0
  20. package/dist/src/login/credentials-store.js +142 -0
  21. package/dist/src/login/index.d.ts +5 -2
  22. package/dist/src/login/index.js +11 -27
  23. package/dist/src/login/legacy-jwt.d.ts +4 -0
  24. package/dist/src/login/legacy-jwt.js +18 -0
  25. package/dist/src/plugins/auth/credentials-base-url.d.ts +11 -0
  26. package/dist/src/plugins/auth/credentials-base-url.js +24 -0
  27. package/dist/src/plugins/login/index.d.ts +6 -1
  28. package/dist/src/plugins/login/index.js +154 -14
  29. package/dist/src/plugins/logout/index.d.ts +14 -0
  30. package/dist/src/plugins/logout/index.js +35 -3
  31. package/dist/src/plugins/mcp/index.d.ts +1 -0
  32. package/dist/src/plugins/mcp/index.js +8 -7
  33. package/dist/src/utils/auth/client-credentials.d.ts +16 -0
  34. package/dist/src/utils/auth/client-credentials.js +53 -0
  35. package/dist/src/utils/auth/oauth-flow.d.ts +12 -0
  36. package/dist/src/utils/auth/{login.js → oauth-flow.js} +36 -58
  37. package/dist/src/utils/retry.d.ts +5 -0
  38. package/dist/src/utils/retry.js +21 -0
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +3 -3
  41. package/dist/src/utils/auth/login.d.ts +0 -7
@@ -0,0 +1,142 @@
1
+ import { createHash } from "crypto";
2
+ import { getPassword, setPassword, deletePassword } from "cross-keychain";
3
+ import { z } from "zod";
4
+ import { DEFAULT_AUTH_BASE_URL, getConfig } from "./config";
5
+ import { enqueue, getBackendInfo } from "./keychain";
6
+ const SERVICE = "zapier-sdk-cli";
7
+ const CREDENTIALS_KEY = "credentials";
8
+ const REGISTRY_KEY = "credentialsRegistry";
9
+ const CredentialsEntrySchema = z.object({
10
+ name: z.string(),
11
+ clientId: z.string(),
12
+ createdAt: z.number(),
13
+ scopes: z.array(z.string()),
14
+ baseUrl: z.string(),
15
+ });
16
+ function normalizeBaseUrl(baseUrl) {
17
+ return baseUrl ?? DEFAULT_AUTH_BASE_URL;
18
+ }
19
+ function keychainAccount(key) {
20
+ return createHash("sha256").update(key).digest("hex");
21
+ }
22
+ function buildKeychainKey(clientId, scopes, baseUrl) {
23
+ const sortedScopes = [...scopes].sort().join(",");
24
+ return `zapier-sdk/client-credentials-secret/${clientId}:${sortedScopes}:${baseUrl}`;
25
+ }
26
+ function findEntry(registry, name, baseUrl) {
27
+ return registry.find((e) => e.name === name && e.baseUrl === baseUrl);
28
+ }
29
+ function readRegistry() {
30
+ const stored = getConfig().get(REGISTRY_KEY);
31
+ if (!Array.isArray(stored))
32
+ return [];
33
+ return stored.flatMap((entry) => {
34
+ const result = CredentialsEntrySchema.safeParse(entry);
35
+ return result.success ? [result.data] : [];
36
+ });
37
+ }
38
+ export function getActiveCredentials(options) {
39
+ const name = getConfig().get(CREDENTIALS_KEY);
40
+ if (!name)
41
+ return undefined;
42
+ return findEntry(readRegistry(), name, normalizeBaseUrl(options?.baseUrl));
43
+ }
44
+ export async function storeClientCredentials({ name, clientId, clientSecret, scopes, baseUrl, }) {
45
+ if (!name || typeof name !== "string") {
46
+ throw new Error("storeClientCredentials: name is required");
47
+ }
48
+ if (!clientId || typeof clientId !== "string") {
49
+ throw new Error("storeClientCredentials: clientId is required");
50
+ }
51
+ if (!clientSecret || typeof clientSecret !== "string") {
52
+ throw new Error("storeClientCredentials: clientSecret is required");
53
+ }
54
+ if (!Array.isArray(scopes) || scopes.length === 0) {
55
+ throw new Error("storeClientCredentials: scopes must be a non-empty array");
56
+ }
57
+ const sortedScopes = [...scopes].sort();
58
+ const resolvedBaseUrl = normalizeBaseUrl(baseUrl);
59
+ const keychainKey = buildKeychainKey(clientId, sortedScopes, resolvedBaseUrl);
60
+ const existingEntry = findEntry(readRegistry(), name, resolvedBaseUrl);
61
+ const existingKeychainKey = existingEntry
62
+ ? buildKeychainKey(existingEntry.clientId, existingEntry.scopes, existingEntry.baseUrl)
63
+ : undefined;
64
+ await enqueue(async () => {
65
+ await getBackendInfo();
66
+ await setPassword(SERVICE, keychainAccount(keychainKey), clientSecret);
67
+ });
68
+ const entry = {
69
+ name,
70
+ clientId,
71
+ createdAt: Date.now(),
72
+ scopes: sortedScopes,
73
+ baseUrl: resolvedBaseUrl,
74
+ };
75
+ const registry = readRegistry().filter((e) => !(e.name === name && e.baseUrl === resolvedBaseUrl));
76
+ registry.push(entry);
77
+ const cfg = getConfig();
78
+ cfg.set(REGISTRY_KEY, registry);
79
+ cfg.set(CREDENTIALS_KEY, name);
80
+ if (existingEntry && existingKeychainKey !== keychainKey) {
81
+ await deleteKeychainSecret(existingEntry);
82
+ }
83
+ }
84
+ export function credentialsNameExists({ name, baseUrl, }) {
85
+ return !!findEntry(readRegistry(), name, normalizeBaseUrl(baseUrl));
86
+ }
87
+ export async function getStoredClientCredentials(options) {
88
+ const entry = options?.name
89
+ ? findEntry(readRegistry(), options.name, normalizeBaseUrl(options.baseUrl))
90
+ : getActiveCredentials(options);
91
+ if (!entry)
92
+ return undefined;
93
+ const keychainKey = buildKeychainKey(entry.clientId, entry.scopes, entry.baseUrl);
94
+ const clientSecret = await enqueue(async () => {
95
+ await getBackendInfo();
96
+ return getPassword(SERVICE, keychainAccount(keychainKey));
97
+ });
98
+ if (!clientSecret)
99
+ return undefined;
100
+ return {
101
+ type: "client_credentials",
102
+ clientId: entry.clientId,
103
+ clientSecret,
104
+ baseUrl: entry.baseUrl,
105
+ scope: [...entry.scopes].sort().join(" "),
106
+ };
107
+ }
108
+ function deleteRegistryEntry(registry, name, baseUrl) {
109
+ const idx = registry.findIndex((e) => e.name === name && e.baseUrl === baseUrl);
110
+ if (idx === -1)
111
+ return undefined;
112
+ const [removed] = registry.splice(idx, 1);
113
+ return removed;
114
+ }
115
+ function unsetMatchingCredentialsKey(cfg, name) {
116
+ const activeName = cfg.get(CREDENTIALS_KEY);
117
+ if (activeName === name && !readRegistry().some((e) => e.name === name)) {
118
+ cfg.delete(CREDENTIALS_KEY);
119
+ }
120
+ }
121
+ async function deleteKeychainSecret(entry) {
122
+ const keychainKey = buildKeychainKey(entry.clientId, entry.scopes, entry.baseUrl);
123
+ try {
124
+ await enqueue(async () => {
125
+ await getBackendInfo();
126
+ await deletePassword(SERVICE, keychainAccount(keychainKey));
127
+ });
128
+ }
129
+ catch {
130
+ // Best-effort; key may already be gone.
131
+ }
132
+ }
133
+ export async function deleteStoredClientCredentials({ name, baseUrl, }) {
134
+ const registry = readRegistry();
135
+ const removed = deleteRegistryEntry(registry, name, normalizeBaseUrl(baseUrl));
136
+ if (!removed)
137
+ return;
138
+ const cfg = getConfig();
139
+ cfg.set(REGISTRY_KEY, registry);
140
+ unsetMatchingCredentialsKey(cfg, name);
141
+ await deleteKeychainSecret(removed);
142
+ }
@@ -4,7 +4,7 @@
4
4
  * Handles login, logout, token cache and refresh for Zapier SDK CLI.
5
5
  * Provides getToken function that can be optionally imported by zapier-sdk.
6
6
  */
7
- import Conf from "conf";
7
+ export { DEFAULT_AUTH_BASE_URL, getConfig } from "./config";
8
8
  export { createCache } from "./filesystem-cache";
9
9
  /**
10
10
  * Authentication error for token refresh failures.
@@ -64,12 +64,13 @@ export interface PkceLoginConfig {
64
64
  /**
65
65
  * Gets all configuration needed for the PKCE login flow.
66
66
  * Credentials should have baseUrl already resolved by SDK.
67
+ * baseUrl is a fallback used when credentials carry no baseUrl of their own.
67
68
  */
68
69
  export declare function getPkceLoginConfig(options?: {
69
70
  credentials?: PkceCredentials;
71
+ baseUrl?: string;
70
72
  }): PkceLoginConfig;
71
73
  export type LoginStorageMode = "keychain" | "config";
72
- export declare function getConfig(): Conf;
73
74
  /**
74
75
  * Drops the cached Conf instance so the next getConfig() call re-initializes
75
76
  * from disk (including re-running the pre-existing file detection).
@@ -113,3 +114,5 @@ export declare function logout(options?: Pick<AuthOptions, "onEvent">): Promise<
113
114
  * Gets the path to the configuration file.
114
115
  */
115
116
  export declare function getConfigPath(): string;
117
+ export { getStoredClientCredentials, getActiveCredentials, } from "./credentials-store";
118
+ export { clearTokensFromKeychain } from "./keychain";
@@ -4,10 +4,11 @@
4
4
  * Handles login, logout, token cache and refresh for Zapier SDK CLI.
5
5
  * Provides getToken function that can be optionally imported by zapier-sdk.
6
6
  */
7
- import Conf from "conf";
8
- import { existsSync } from "fs";
9
7
  import * as jwt from "jsonwebtoken";
10
8
  import { getTokensFromKeychain, setTokensInKeychain, clearTokensFromKeychain, } from "./keychain";
9
+ import { clearLegacyJwtConfigKeys } from "./legacy-jwt";
10
+ import { getConfig, resetConfig, DEFAULT_AUTH_BASE_URL } from "./config";
11
+ export { DEFAULT_AUTH_BASE_URL, getConfig } from "./config";
11
12
  export { createCache } from "./filesystem-cache";
12
13
  /**
13
14
  * Authentication error for token refresh failures.
@@ -21,7 +22,6 @@ export class ZapierAuthenticationError extends Error {
21
22
  this.name = "ZapierAuthenticationError";
22
23
  }
23
24
  }
24
- let config = null;
25
25
  // Default OAuth client ID
26
26
  const DEFAULT_AUTH_CLIENT_ID = "grwWZD5hUWGvb4V8ODBuOtXer3h0DBEZ2HR8aay6";
27
27
  // Buffer time before token expiration to trigger refresh (5 minutes)
@@ -58,8 +58,6 @@ function getAuthClientId(clientId) {
58
58
  return clientId || DEFAULT_AUTH_CLIENT_ID;
59
59
  }
60
60
  export const AUTH_MODE_HEADER = "X-Auth";
61
- // Default auth base URL
62
- const DEFAULT_AUTH_BASE_URL = "https://zapier.com";
63
61
  /**
64
62
  * Gets the OAuth token endpoint URL.
65
63
  * baseUrl should be the auth base URL (e.g., https://zapier.com), already resolved by SDK.
@@ -79,37 +77,23 @@ export function getAuthAuthorizeUrl(options) {
79
77
  /**
80
78
  * Gets all configuration needed for the PKCE login flow.
81
79
  * Credentials should have baseUrl already resolved by SDK.
80
+ * baseUrl is a fallback used when credentials carry no baseUrl of their own.
82
81
  */
83
82
  export function getPkceLoginConfig(options) {
83
+ const effectiveBaseUrl = options?.credentials?.baseUrl ?? options?.baseUrl;
84
84
  return {
85
85
  clientId: getAuthClientId(options?.credentials?.clientId),
86
- tokenUrl: getAuthTokenUrl({ baseUrl: options?.credentials?.baseUrl }),
87
- authorizeUrl: getAuthAuthorizeUrl({
88
- baseUrl: options?.credentials?.baseUrl,
89
- }),
86
+ tokenUrl: getAuthTokenUrl({ baseUrl: effectiveBaseUrl }),
87
+ authorizeUrl: getAuthAuthorizeUrl({ baseUrl: effectiveBaseUrl }),
90
88
  };
91
89
  }
92
90
  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
91
  /**
108
92
  * Drops the cached Conf instance so the next getConfig() call re-initializes
109
93
  * from disk (including re-running the pre-existing file detection).
110
94
  */
111
95
  export function unloadConfig() {
112
- config = null;
96
+ resetConfig();
113
97
  cachedLogin = undefined;
114
98
  }
115
99
  export async function updateLogin(loginData, options = {}) {
@@ -424,9 +408,7 @@ export async function logout(options = {}) {
424
408
  await clearTokensFromKeychain();
425
409
  const cfg = getConfig();
426
410
  cfg.set("login_storage_mode", mode);
427
- cfg.delete("login_expires_at");
428
- cfg.delete("login_jwt");
429
- cfg.delete("login_refresh_token");
411
+ clearLegacyJwtConfigKeys(cfg);
430
412
  onEvent?.({
431
413
  type: "auth_logout",
432
414
  payload: { message: "Logged out successfully", operation: "logout" },
@@ -440,3 +422,5 @@ export function getConfigPath() {
440
422
  const cfg = getConfig();
441
423
  return cfg.path;
442
424
  }
425
+ export { getStoredClientCredentials, getActiveCredentials, } from "./credentials-store";
426
+ export { clearTokensFromKeychain } from "./keychain";
@@ -0,0 +1,4 @@
1
+ import type Conf from "conf";
2
+ export declare function clearLegacyJwtConfigKeys(config: Conf): void;
3
+ export declare function clearLegacyJwtState(): Promise<void>;
4
+ export declare function hasLegacyJwtConfig(): boolean;
@@ -0,0 +1,18 @@
1
+ import { getConfig } from "./config";
2
+ import { clearTokensFromKeychain } from "./keychain";
3
+ export function clearLegacyJwtConfigKeys(config) {
4
+ config.delete("login_jwt");
5
+ config.delete("login_refresh_token");
6
+ config.delete("login_expires_at");
7
+ }
8
+ export async function clearLegacyJwtState() {
9
+ clearLegacyJwtConfigKeys(getConfig());
10
+ await clearTokensFromKeychain();
11
+ }
12
+ // Intentionally skips the keychain — conf keys are sufficient and a keychain probe is heavyweight.
13
+ export function hasLegacyJwtConfig() {
14
+ const cfg = getConfig();
15
+ return (typeof cfg.get("login_jwt") === "string" ||
16
+ typeof cfg.get("login_refresh_token") === "string" ||
17
+ typeof cfg.get("login_expires_at") === "number");
18
+ }
@@ -0,0 +1,11 @@
1
+ import { type ResolvedCredentials } from "@zapier/zapier-sdk";
2
+ interface BaseUrlContext {
3
+ resolveCredentials?: () => Promise<ResolvedCredentials | undefined>;
4
+ resolvedCredentials?: ResolvedCredentials | undefined;
5
+ options?: {
6
+ baseUrl?: string;
7
+ credentials?: unknown;
8
+ };
9
+ }
10
+ export declare function resolveCredentialsBaseUrl(context: BaseUrlContext): Promise<string | undefined>;
11
+ export {};
@@ -0,0 +1,24 @@
1
+ import { isCredentialsObject, } from "@zapier/zapier-sdk";
2
+ function getBaseUrlFromResolvedCredentials(credentials) {
3
+ if (credentials && isCredentialsObject(credentials)) {
4
+ return credentials.baseUrl;
5
+ }
6
+ return undefined;
7
+ }
8
+ function getBaseUrlFromOptionsCredentials(credentials) {
9
+ if (credentials &&
10
+ typeof credentials === "object" &&
11
+ "baseUrl" in credentials &&
12
+ typeof credentials.baseUrl === "string") {
13
+ return credentials.baseUrl;
14
+ }
15
+ return undefined;
16
+ }
17
+ export async function resolveCredentialsBaseUrl(context) {
18
+ const resolvedCredentials = "resolvedCredentials" in context
19
+ ? context.resolvedCredentials
20
+ : await context.resolveCredentials?.();
21
+ return (getBaseUrlFromResolvedCredentials(resolvedCredentials) ??
22
+ getBaseUrlFromOptionsCredentials(context.options?.credentials) ??
23
+ context.options?.baseUrl);
24
+ }
@@ -1,10 +1,15 @@
1
- import type { ResolvedCredentials } from "@zapier/zapier-sdk";
1
+ import { type ApiClient, type ResolvedCredentials } from "@zapier/zapier-sdk";
2
2
  interface CliContext {
3
3
  session_id?: string | null;
4
4
  selected_api?: string | null;
5
5
  app_id?: number | null;
6
6
  app_version_id?: number | null;
7
7
  resolveCredentials: () => Promise<ResolvedCredentials | undefined>;
8
+ api: ApiClient;
9
+ options?: {
10
+ baseUrl?: string;
11
+ credentials?: unknown;
12
+ };
8
13
  }
9
14
  export declare const loginPlugin: (sdk: {
10
15
  context: import("@zapier/zapier-sdk").EventEmissionContext;
@@ -1,6 +1,12 @@
1
- import { createPluginMethod, definePlugin, isCredentialsObject, buildApplicationLifecycleEvent, } from "@zapier/zapier-sdk";
2
- import login from "../../utils/auth/login";
3
- import { getLoggedInUser } from "../../login";
1
+ import { hostname } from "os";
2
+ import { createPluginMethod, definePlugin, getOrCreateApiClient, isCredentialsObject, buildApplicationLifecycleEvent, } from "@zapier/zapier-sdk";
3
+ import inquirer from "inquirer";
4
+ import { clearLegacyJwtState, hasLegacyJwtConfig, } from "../../login/legacy-jwt";
5
+ import { getActiveCredentials, credentialsNameExists, deleteStoredClientCredentials, } from "../../login/credentials-store";
6
+ import { revokeCredentials } from "../../login/credentials-revoke";
7
+ import { runOauthFlow } from "../../utils/auth/oauth-flow";
8
+ import { setupClientCredentials } from "../../utils/auth/client-credentials";
9
+ import { resolveCredentialsBaseUrl } from "../auth/credentials-base-url";
4
10
  import { LoginSchema } from "./schemas";
5
11
  function toPkceCredentials(credentials) {
6
12
  if (credentials &&
@@ -15,26 +21,160 @@ function toPkceCredentials(credentials) {
15
21
  }
16
22
  return undefined;
17
23
  }
24
+ async function confirmRevokeAndRelogin(activeCredentials) {
25
+ const { confirmed } = await inquirer.prompt([
26
+ {
27
+ type: "confirm",
28
+ name: "confirmed",
29
+ message: `You are already logged in as "${activeCredentials.name}".\n` +
30
+ "Logging out will delete these credentials and may interrupt other Zapier SDK or CLI sessions using them.\n" +
31
+ "Log out and log in again?",
32
+ default: false,
33
+ },
34
+ ]);
35
+ if (!confirmed) {
36
+ console.log("Login cancelled.");
37
+ return false;
38
+ }
39
+ return true;
40
+ }
41
+ async function confirmJwtMigration() {
42
+ const { confirmed } = await inquirer.prompt([
43
+ {
44
+ type: "confirm",
45
+ name: "confirmed",
46
+ message: "We're upgrading your login to client credentials for a simpler, more reliable experience " +
47
+ "and to support future security controls. " +
48
+ "Older Zapier SDK/CLI versions on this machine may stop working after the upgrade. " +
49
+ "Continue?",
50
+ default: true,
51
+ },
52
+ ]);
53
+ if (!confirmed) {
54
+ console.log("Login cancelled.");
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+ async function confirmLocalLoginReset() {
60
+ const { confirmed } = await inquirer.prompt([
61
+ {
62
+ type: "confirm",
63
+ name: "confirmed",
64
+ message: "Login cleanup failed. Reset local session state and continue?",
65
+ default: false,
66
+ },
67
+ ]);
68
+ if (!confirmed) {
69
+ console.log("Login cancelled.");
70
+ return false;
71
+ }
72
+ return true;
73
+ }
74
+ function parseTimeoutSeconds(timeout) {
75
+ const timeoutSeconds = timeout ? parseInt(timeout, 10) : 300;
76
+ if (isNaN(timeoutSeconds) || timeoutSeconds <= 0) {
77
+ throw new Error("Timeout must be a positive number");
78
+ }
79
+ return timeoutSeconds;
80
+ }
81
+ async function promptCredentialsName(email, baseUrl) {
82
+ const { credentialName } = await inquirer.prompt([
83
+ {
84
+ type: "input",
85
+ name: "credentialName",
86
+ message: "Enter a name to identify them:",
87
+ default: `${email}@${hostname()}`,
88
+ validate: (input) => {
89
+ if (!input.trim())
90
+ return "Name cannot be empty";
91
+ if (credentialsNameExists({ name: input.trim(), baseUrl })) {
92
+ return `Credentials named "${input.trim()}" already exist. Please provide a different name.`;
93
+ }
94
+ return true;
95
+ },
96
+ },
97
+ ]);
98
+ return credentialName;
99
+ }
100
+ function emitLoginSuccess({ sdk, profile, }) {
101
+ sdk.context.eventEmission.emit("platform.sdk.ApplicationLifecycleEvent", buildApplicationLifecycleEvent({ lifecycle_event_type: "login_success" }, {
102
+ customuser_id: profile.user_id,
103
+ account_id: profile.roles[0]?.account_id ?? null,
104
+ }));
105
+ }
106
+ async function getProfile(api) {
107
+ return api.get("/zapier/api/v4/profile/", {
108
+ authRequired: true,
109
+ });
110
+ }
111
+ async function bestEffortClearLegacyJwtState() {
112
+ // Don't fail the whole login over a transient keychain hiccup during cleanup.
113
+ try {
114
+ await clearLegacyJwtState();
115
+ }
116
+ catch (err) {
117
+ console.error("[login] Best-effort legacy JWT cleanup failed:", err);
118
+ }
119
+ }
18
120
  export const loginPlugin = definePlugin((sdk) => createPluginMethod(sdk, {
19
121
  name: "login",
20
122
  categories: ["account"],
21
123
  inputSchema: LoginSchema,
22
124
  supportsJsonOutput: false,
23
125
  handler: async ({ sdk, options }) => {
24
- const timeoutSeconds = options.timeout
25
- ? parseInt(options.timeout, 10)
26
- : 300;
27
- if (isNaN(timeoutSeconds) || timeoutSeconds <= 0) {
28
- throw new Error("Timeout must be a positive number");
29
- }
126
+ const timeoutSeconds = parseTimeoutSeconds(options.timeout);
30
127
  const resolvedCredentials = await sdk.context.resolveCredentials();
31
128
  const pkceCredentials = toPkceCredentials(resolvedCredentials);
32
- await login({
129
+ const credentialsBaseUrl = await resolveCredentialsBaseUrl({
130
+ ...sdk.context,
131
+ resolvedCredentials,
132
+ });
133
+ const activeCredentials = getActiveCredentials({
134
+ baseUrl: credentialsBaseUrl,
135
+ });
136
+ if (activeCredentials) {
137
+ if (!(await confirmRevokeAndRelogin(activeCredentials)))
138
+ return;
139
+ try {
140
+ await revokeCredentials({
141
+ api: sdk.context.api,
142
+ credentials: activeCredentials,
143
+ });
144
+ }
145
+ catch {
146
+ if (!(await confirmLocalLoginReset()))
147
+ return;
148
+ await deleteStoredClientCredentials({
149
+ name: activeCredentials.name,
150
+ baseUrl: activeCredentials.baseUrl,
151
+ });
152
+ }
153
+ }
154
+ else if (hasLegacyJwtConfig()) {
155
+ if (!(await confirmJwtMigration()))
156
+ return;
157
+ }
158
+ const { accessToken } = await runOauthFlow({
33
159
  timeoutMs: timeoutSeconds * 1000,
34
- credentials: pkceCredentials,
160
+ pkceCredentials,
161
+ baseUrl: credentialsBaseUrl,
162
+ });
163
+ const scopedApi = getOrCreateApiClient({
164
+ credentials: accessToken,
165
+ baseUrl: credentialsBaseUrl,
166
+ });
167
+ const profile = await getProfile(scopedApi);
168
+ console.log(`👤 Logged in as ${profile.email}`);
169
+ console.log("\nGenerating credentials so this machine can make authenticated requests on your behalf.");
170
+ const credentialName = await promptCredentialsName(profile.email, credentialsBaseUrl);
171
+ await setupClientCredentials({
172
+ api: scopedApi,
173
+ name: credentialName,
174
+ credentialsBaseUrl,
35
175
  });
36
- const user = await getLoggedInUser();
37
- sdk.context.eventEmission.emit("platform.sdk.ApplicationLifecycleEvent", buildApplicationLifecycleEvent({ lifecycle_event_type: "login_success" }, { customuser_id: user.customUserId, account_id: user.accountId }));
38
- console.log(`✅ Successfully logged in as ${user.email}`);
176
+ await bestEffortClearLegacyJwtState();
177
+ console.log(`✅ Credentials "${credentialName}" created and set as default. You are ready to use the Zapier SDK.`);
178
+ emitLoginSuccess({ sdk, profile });
39
179
  },
40
180
  }));
@@ -1,5 +1,18 @@
1
+ import { type ApiClient, type ResolvedCredentials } from "@zapier/zapier-sdk";
2
+ import { type LogoutEventEmitter } from "../../login/credentials-revoke";
3
+ interface CliContext {
4
+ api: ApiClient;
5
+ resolveCredentials?: () => Promise<ResolvedCredentials | undefined>;
6
+ options?: {
7
+ onEvent?: LogoutEventEmitter;
8
+ baseUrl?: string;
9
+ credentials?: unknown;
10
+ };
11
+ }
1
12
  export declare const logoutPlugin: (sdk: {
2
13
  context: import("@zapier/zapier-sdk").EventEmissionContext;
14
+ } & {
15
+ context: CliContext;
3
16
  } & {
4
17
  context: {
5
18
  meta: Record<string, import("@zapier/zapier-sdk").PluginMeta>;
@@ -14,3 +27,4 @@ export declare const logoutPlugin: (sdk: {
14
27
  };
15
28
  };
16
29
  export type LogoutPluginProvides = ReturnType<typeof logoutPlugin>;
30
+ export {};
@@ -1,13 +1,45 @@
1
1
  import { createPluginMethod, definePlugin, } from "@zapier/zapier-sdk";
2
- import { logout } from "../../login";
2
+ import inquirer from "inquirer";
3
+ import { logout as jwtLogout } from "../../login";
4
+ import { getActiveCredentials } from "../../login/credentials-store";
5
+ import { revokeCredentials, } from "../../login/credentials-revoke";
6
+ import { resolveCredentialsBaseUrl } from "../auth/credentials-base-url";
3
7
  import { LogoutSchema } from "./schemas";
4
8
  export const logoutPlugin = definePlugin((sdk) => createPluginMethod(sdk, {
5
9
  name: "logout",
6
10
  categories: ["account"],
7
11
  inputSchema: LogoutSchema,
8
12
  supportsJsonOutput: false,
9
- handler: async () => {
10
- await logout();
13
+ handler: async ({ sdk }) => {
14
+ const credentialsBaseUrl = await resolveCredentialsBaseUrl(sdk.context);
15
+ const activeCredentials = getActiveCredentials({
16
+ baseUrl: credentialsBaseUrl,
17
+ });
18
+ const onEvent = sdk.context.options?.onEvent;
19
+ if (!activeCredentials) {
20
+ await jwtLogout({ onEvent });
21
+ console.log("✅ Successfully logged out");
22
+ return;
23
+ }
24
+ const { confirmed } = await inquirer.prompt([
25
+ {
26
+ type: "confirm",
27
+ name: "confirmed",
28
+ message: `Logging out will delete credentials "${activeCredentials.name}".\n` +
29
+ "This may interrupt other Zapier SDK or CLI sessions using them.\n" +
30
+ "Do you want to continue?",
31
+ default: true,
32
+ },
33
+ ]);
34
+ if (!confirmed) {
35
+ console.log("Logout cancelled.");
36
+ return;
37
+ }
38
+ await revokeCredentials({
39
+ api: sdk.context.api,
40
+ credentials: activeCredentials,
41
+ onEvent,
42
+ });
11
43
  console.log("✅ Successfully logged out");
12
44
  },
13
45
  }));
@@ -3,6 +3,7 @@ export declare const mcpPlugin: (sdk: {
3
3
  context: {
4
4
  options?: {
5
5
  debug?: boolean;
6
+ maxConcurrentRequests?: number;
6
7
  };
7
8
  extensions?: Plugin<unknown, PluginProvides>[];
8
9
  experimental?: boolean;
@@ -6,16 +6,17 @@ export const mcpPlugin = definePlugin((sdk) => createPluginMethod(sdk, {
6
6
  categories: ["utility"],
7
7
  inputSchema: McpSchema,
8
8
  handler: async ({ sdk, options }) => {
9
- // Forward debug + the extensions resolved at CLI startup so the MCP
10
- // server's registry includes extension functions as tools. Without
11
- // this forward, MCP would build a vanilla SDK and the CLI/MCP
12
- // surfaces would diverge. The `experimental` flag (set by the
13
- // experimental CLI factory) tells the MCP server to build against
14
- // `@zapier/zapier-sdk/experimental` so experimental tools are
15
- // exposed.
9
+ // Forward debug, the concurrency cap, and the extensions resolved at
10
+ // CLI startup so the MCP server's SDK matches what the user asked
11
+ // for at the CLI surface. Without this forward, MCP would build a
12
+ // vanilla SDK and the CLI/MCP surfaces would diverge. The
13
+ // `experimental` flag (set by the experimental CLI factory) tells
14
+ // the MCP server to build against `@zapier/zapier-sdk/experimental`
15
+ // so experimental tools are exposed.
16
16
  await startMcpServer({
17
17
  ...options,
18
18
  debug: sdk.context.options?.debug,
19
+ maxConcurrentRequests: sdk.context.options?.maxConcurrentRequests,
19
20
  extensions: sdk.context.extensions,
20
21
  experimental: sdk.context.experimental,
21
22
  });
@@ -0,0 +1,16 @@
1
+ import type { ApiClient } from "@zapier/zapier-sdk";
2
+ export interface SetupClientCredentialsOptions {
3
+ api: ApiClient;
4
+ name: string;
5
+ credentialsBaseUrl?: string;
6
+ }
7
+ export interface SetupClientCredentialsResult {
8
+ clientId: string;
9
+ }
10
+ /**
11
+ * Create a client credential server-side and persist it locally. If the
12
+ * local store fails after the server-side credential is created, attempt
13
+ * to roll back the orphan; if that rollback also fails, surface a
14
+ * manual-fix message and re-throw the original store error.
15
+ */
16
+ export declare function setupClientCredentials({ api, name, credentialsBaseUrl, }: SetupClientCredentialsOptions): Promise<SetupClientCredentialsResult>;