@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.
- package/CHANGELOG.md +22 -0
- package/README.md +2 -1
- package/dist/cli.cjs +581 -86
- package/dist/cli.mjs +581 -86
- package/dist/experimental.cjs +562 -86
- package/dist/experimental.mjs +561 -85
- package/dist/index.cjs +563 -87
- package/dist/index.mjs +562 -86
- package/dist/login.cjs +94 -25
- package/dist/login.d.mts +29 -2
- package/dist/login.d.ts +29 -2
- package/dist/login.mjs +90 -25
- package/dist/package.json +1 -1
- package/dist/src/cli.js +32 -3
- package/dist/src/login/config.d.ts +4 -0
- package/dist/src/login/config.js +21 -0
- package/dist/src/login/credentials-revoke.d.ts +13 -0
- package/dist/src/login/credentials-revoke.js +48 -0
- package/dist/src/login/credentials-store.d.ts +33 -0
- package/dist/src/login/credentials-store.js +142 -0
- package/dist/src/login/index.d.ts +5 -2
- package/dist/src/login/index.js +11 -27
- package/dist/src/login/legacy-jwt.d.ts +4 -0
- package/dist/src/login/legacy-jwt.js +18 -0
- package/dist/src/plugins/auth/credentials-base-url.d.ts +11 -0
- package/dist/src/plugins/auth/credentials-base-url.js +24 -0
- package/dist/src/plugins/login/index.d.ts +6 -1
- package/dist/src/plugins/login/index.js +154 -14
- package/dist/src/plugins/logout/index.d.ts +14 -0
- package/dist/src/plugins/logout/index.js +35 -3
- package/dist/src/plugins/mcp/index.d.ts +1 -0
- package/dist/src/plugins/mcp/index.js +8 -7
- package/dist/src/utils/auth/client-credentials.d.ts +16 -0
- package/dist/src/utils/auth/client-credentials.js +53 -0
- package/dist/src/utils/auth/oauth-flow.d.ts +12 -0
- package/dist/src/utils/auth/{login.js → oauth-flow.js} +36 -58
- package/dist/src/utils/retry.d.ts +5 -0
- package/dist/src/utils/retry.js +21 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- 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
|
-
|
|
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";
|
package/dist/src/login/index.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
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,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
|
|
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 {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
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
|
}));
|
|
@@ -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
|
|
10
|
-
// server's
|
|
11
|
-
// this forward, MCP would build a
|
|
12
|
-
// surfaces would diverge. The
|
|
13
|
-
// experimental
|
|
14
|
-
// `@zapier/zapier-sdk/experimental`
|
|
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>;
|