@zapier/zapier-sdk-cli 0.42.1 → 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 +21 -0
- package/README.md +4 -4
- 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 +6 -3
- 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 +8 -5
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default filesystem + OS keychain cache adapter for the SDK.
|
|
3
|
+
*
|
|
4
|
+
* Conforms to the generic ZapierCache interface that the SDK defines.
|
|
5
|
+
* Secrets go to the OS keychain via cross-keychain; non-secret values
|
|
6
|
+
* and the identity metadata (expires_at, secret flag) go to the
|
|
7
|
+
* existing zapier-sdk-cli Conf file. A proper-lockfile lock wraps the
|
|
8
|
+
* exchange/persist critical section so N concurrent CLI invocations
|
|
9
|
+
* collapse to a single network exchange.
|
|
10
|
+
*
|
|
11
|
+
* The SDK treats this (like any ZapierCache) as a cache — values may
|
|
12
|
+
* disappear, and the caller must always be able to recreate them.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "crypto";
|
|
15
|
+
import { dirname } from "path";
|
|
16
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
17
|
+
import { getPassword, setPassword, deletePassword } from "cross-keychain";
|
|
18
|
+
import * as lockfile from "proper-lockfile";
|
|
19
|
+
import { getConfig } from "./index";
|
|
20
|
+
import { enqueue, getBackendInfo } from "./keychain";
|
|
21
|
+
const SERVICE = "zapier-sdk-cache";
|
|
22
|
+
const CONFIG_KEY = "cache";
|
|
23
|
+
const LOCK_UPDATE_MS = 5000;
|
|
24
|
+
const LOCK_STALE_MS = 10000;
|
|
25
|
+
const LOCK_RETRY_WAIT_MS = 100;
|
|
26
|
+
const LOCK_RETRY_MAX_WAIT_MS = 1000;
|
|
27
|
+
const LOCK_RETRY_COUNT = 120; // ~2 min worst-case wait under heavy contention
|
|
28
|
+
/**
|
|
29
|
+
* cross-keychain's native macOS backend restricts account names to
|
|
30
|
+
* alphanumeric + . _ @ -. Hash arbitrary SDK keys to a hex digest to
|
|
31
|
+
* stay inside that alphabet.
|
|
32
|
+
*/
|
|
33
|
+
function keychainAccount(key) {
|
|
34
|
+
return createHash("sha256").update(key).digest("hex");
|
|
35
|
+
}
|
|
36
|
+
function readConfigMap() {
|
|
37
|
+
const cfg = getConfig();
|
|
38
|
+
const stored = cfg.get(CONFIG_KEY);
|
|
39
|
+
if (stored && typeof stored === "object") {
|
|
40
|
+
return stored;
|
|
41
|
+
}
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
function writeConfigMap(map) {
|
|
45
|
+
getConfig().set(CONFIG_KEY, map);
|
|
46
|
+
}
|
|
47
|
+
function entryIsExpired(entry) {
|
|
48
|
+
return entry.expires_at !== undefined && entry.expires_at <= Date.now();
|
|
49
|
+
}
|
|
50
|
+
export function createCache() {
|
|
51
|
+
return {
|
|
52
|
+
async get(key) {
|
|
53
|
+
const entry = readConfigMap()[key];
|
|
54
|
+
if (!entry)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (entryIsExpired(entry))
|
|
57
|
+
return undefined;
|
|
58
|
+
if (entry.secret) {
|
|
59
|
+
const stored = await enqueue(async () => {
|
|
60
|
+
await getBackendInfo();
|
|
61
|
+
return getPassword(SERVICE, keychainAccount(key));
|
|
62
|
+
});
|
|
63
|
+
if (!stored) {
|
|
64
|
+
// Config claims a secret exists but keychain doesn't have it.
|
|
65
|
+
// Treat as a miss so the caller re-produces the value.
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return { value: stored, expiresAt: entry.expires_at };
|
|
69
|
+
}
|
|
70
|
+
if (entry.value === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
return { value: entry.value, expiresAt: entry.expires_at };
|
|
73
|
+
},
|
|
74
|
+
async set(key, value, options) {
|
|
75
|
+
const secret = options?.secret ?? false;
|
|
76
|
+
const expiresAt = options?.ttl
|
|
77
|
+
? Date.now() + options.ttl * 1000
|
|
78
|
+
: undefined;
|
|
79
|
+
if (secret) {
|
|
80
|
+
// Keychain first so a failed keychain write doesn't leave a
|
|
81
|
+
// dangling config pointer to a non-existent secret.
|
|
82
|
+
try {
|
|
83
|
+
await enqueue(async () => {
|
|
84
|
+
await getBackendInfo();
|
|
85
|
+
await setPassword(SERVICE, keychainAccount(key), value);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// No keychain available (headless CI, sandboxed env). Skip
|
|
90
|
+
// the config write too so subsequent reads cleanly miss.
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const map = readConfigMap();
|
|
94
|
+
map[key] = { secret: true, expires_at: expiresAt };
|
|
95
|
+
try {
|
|
96
|
+
writeConfigMap(map);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Config is read-only; orphan the keychain entry. Reads find
|
|
100
|
+
// no config pointer and treat as a miss — self-healing on the
|
|
101
|
+
// next successful write.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const map = readConfigMap();
|
|
106
|
+
map[key] = { secret: false, value, expires_at: expiresAt };
|
|
107
|
+
try {
|
|
108
|
+
writeConfigMap(map);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Nothing we can do; next write for this key is the next
|
|
112
|
+
// chance to persist.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
async delete(key) {
|
|
117
|
+
// Config first: an orphan keychain entry with no config pointer
|
|
118
|
+
// is unreachable via `get`, which is better than a stuck stale
|
|
119
|
+
// value if the keychain delete succeeds but config write fails.
|
|
120
|
+
const map = readConfigMap();
|
|
121
|
+
const entry = map[key];
|
|
122
|
+
if (entry) {
|
|
123
|
+
delete map[key];
|
|
124
|
+
try {
|
|
125
|
+
writeConfigMap(map);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// ignore
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (entry?.secret) {
|
|
132
|
+
try {
|
|
133
|
+
await enqueue(async () => {
|
|
134
|
+
await getBackendInfo();
|
|
135
|
+
await deletePassword(SERVICE, keychainAccount(key));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Best-effort
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
async withLock(_key, fn) {
|
|
144
|
+
// Global lock on the Conf file: Conf writes the whole file
|
|
145
|
+
// atomically, so a per-key lock wouldn't prevent sibling keys
|
|
146
|
+
// from clobbering each other. Fleet-wide serialization is fine
|
|
147
|
+
// at CLI scale (<1 request/second sustained per machine).
|
|
148
|
+
const cfg = getConfig();
|
|
149
|
+
const lockTarget = `${cfg.path}.cache-lock`;
|
|
150
|
+
try {
|
|
151
|
+
mkdirSync(dirname(lockTarget), { recursive: true });
|
|
152
|
+
if (!existsSync(lockTarget)) {
|
|
153
|
+
writeFileSync(lockTarget, "");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Can't create the lock file (read-only fs, no HOME, etc.).
|
|
158
|
+
// Proceed unsynchronized — correctness is maintained at the
|
|
159
|
+
// cost of possibly N exchanges in the worst case, same as
|
|
160
|
+
// before this MR.
|
|
161
|
+
return fn();
|
|
162
|
+
}
|
|
163
|
+
let release = null;
|
|
164
|
+
try {
|
|
165
|
+
release = await lockfile.lock(lockTarget, {
|
|
166
|
+
stale: LOCK_STALE_MS,
|
|
167
|
+
update: LOCK_UPDATE_MS,
|
|
168
|
+
retries: {
|
|
169
|
+
retries: LOCK_RETRY_COUNT,
|
|
170
|
+
factor: 1.2,
|
|
171
|
+
minTimeout: LOCK_RETRY_WAIT_MS,
|
|
172
|
+
maxTimeout: LOCK_RETRY_MAX_WAIT_MS,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Lock is genuinely unavailable. Same graceful-degrade as above.
|
|
178
|
+
return fn();
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
return await fn();
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
try {
|
|
185
|
+
await release();
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// If release fails (e.g. the lockfile was reclaimed as stale
|
|
189
|
+
// while we held it), there's nothing useful to do — the next
|
|
190
|
+
// acquirer will handle it.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
export { createCache } from "./filesystem-cache";
|
|
9
|
+
/**
|
|
10
|
+
* Authentication error for token refresh failures.
|
|
11
|
+
* This is a standalone clone of ZapierAuthenticationError from the SDK.
|
|
12
|
+
* We can't import from SDK because cli-login must remain a standalone package
|
|
13
|
+
* with no SDK dependencies. The SDK recognizes this error by name.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ZapierAuthenticationError extends Error {
|
|
16
|
+
constructor(message: string);
|
|
17
|
+
}
|
|
18
|
+
export interface PkceCredentials {
|
|
19
|
+
type: "pkce";
|
|
20
|
+
clientId: string;
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
scope?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface AuthOptions {
|
|
25
|
+
onEvent?: (event: {
|
|
26
|
+
type: string;
|
|
27
|
+
payload: Record<string, unknown>;
|
|
28
|
+
timestamp: number;
|
|
29
|
+
}) => void;
|
|
30
|
+
fetch?: typeof globalThis.fetch;
|
|
31
|
+
/** Credentials object from SDK. Contains clientId and resolved auth baseUrl. */
|
|
32
|
+
credentials?: PkceCredentials;
|
|
33
|
+
/** Enable debug logging for fetch requests and config operations. */
|
|
34
|
+
debug?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface LoginData {
|
|
37
|
+
access_token: string;
|
|
38
|
+
refresh_token: string;
|
|
39
|
+
expires_in: number;
|
|
40
|
+
}
|
|
41
|
+
export declare const AUTH_MODE_HEADER = "X-Auth";
|
|
42
|
+
/**
|
|
43
|
+
* Gets the OAuth token endpoint URL.
|
|
44
|
+
* baseUrl should be the auth base URL (e.g., https://zapier.com), already resolved by SDK.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getAuthTokenUrl(options?: {
|
|
47
|
+
baseUrl?: string;
|
|
48
|
+
}): string;
|
|
49
|
+
/**
|
|
50
|
+
* Gets the OAuth authorization endpoint URL.
|
|
51
|
+
* baseUrl should be the auth base URL (e.g., https://zapier.com), already resolved by SDK.
|
|
52
|
+
*/
|
|
53
|
+
export declare function getAuthAuthorizeUrl(options?: {
|
|
54
|
+
baseUrl?: string;
|
|
55
|
+
}): string;
|
|
56
|
+
/**
|
|
57
|
+
* Configuration needed for PKCE login flow.
|
|
58
|
+
*/
|
|
59
|
+
export interface PkceLoginConfig {
|
|
60
|
+
clientId: string;
|
|
61
|
+
tokenUrl: string;
|
|
62
|
+
authorizeUrl: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Gets all configuration needed for the PKCE login flow.
|
|
66
|
+
* Credentials should have baseUrl already resolved by SDK.
|
|
67
|
+
*/
|
|
68
|
+
export declare function getPkceLoginConfig(options?: {
|
|
69
|
+
credentials?: PkceCredentials;
|
|
70
|
+
}): PkceLoginConfig;
|
|
71
|
+
export type LoginStorageMode = "keychain" | "config";
|
|
72
|
+
export declare function getConfig(): Conf;
|
|
73
|
+
/**
|
|
74
|
+
* Drops the cached Conf instance so the next getConfig() call re-initializes
|
|
75
|
+
* from disk (including re-running the pre-existing file detection).
|
|
76
|
+
*/
|
|
77
|
+
export declare function unloadConfig(): void;
|
|
78
|
+
export declare function updateLogin(loginData: LoginData, options?: {
|
|
79
|
+
storage?: LoginStorageMode;
|
|
80
|
+
debug?: boolean;
|
|
81
|
+
}): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Main function exported for zapier-sdk to optionally use.
|
|
84
|
+
* Returns the stored token from CLI configuration (with auto-refresh).
|
|
85
|
+
*
|
|
86
|
+
* Note: Environment variable handling (ZAPIER_CREDENTIALS, ZAPIER_TOKEN)
|
|
87
|
+
* is done by the SDK before calling this function.
|
|
88
|
+
*
|
|
89
|
+
* Returns undefined if no valid token is found.
|
|
90
|
+
*/
|
|
91
|
+
export declare function getToken(options?: AuthOptions): Promise<string | undefined>;
|
|
92
|
+
/**
|
|
93
|
+
* Gets the logged-in user information from JWT token.
|
|
94
|
+
* Automatically refreshes token if expired.
|
|
95
|
+
*/
|
|
96
|
+
export declare function getLoggedInUser(options?: AuthOptions): Promise<{
|
|
97
|
+
accountId: number;
|
|
98
|
+
customUserId: number;
|
|
99
|
+
email: string;
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* Checks config to determine current cache mode without reading keychain.
|
|
103
|
+
* Credential keys in config are ground truth — if login_jwt exists, we're in
|
|
104
|
+
* config mode regardless of the marker. The marker only matters when no
|
|
105
|
+
* credential keys are present (e.g. after logout or when using keychain).
|
|
106
|
+
*/
|
|
107
|
+
export declare function getLoginStorageMode(): LoginStorageMode;
|
|
108
|
+
/**
|
|
109
|
+
* Clears stored login information.
|
|
110
|
+
*/
|
|
111
|
+
export declare function logout(options?: Pick<AuthOptions, "onEvent">): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Gets the path to the configuration file.
|
|
114
|
+
*/
|
|
115
|
+
export declare function getConfigPath(): string;
|