@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.
@@ -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;