@circleback/cli 0.3.3 → 0.3.5

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.
@@ -4,9 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.forceRefreshOAuthToken = exports.getValidOAuthTokens = exports.login = void 0;
7
- const node_child_process_1 = require("node:child_process");
8
7
  const node_crypto_1 = __importDefault(require("node:crypto"));
9
8
  const constants_1 = require("../constants");
9
+ const stdin_1 = require("../helpers/stdin");
10
+ const url_1 = require("../helpers/url");
10
11
  const types_1 = require("../types");
11
12
  const server_1 = require("./server");
12
13
  const tokenStorage_1 = require("./tokenStorage");
@@ -89,9 +90,10 @@ const refreshAccessToken = async (refreshToken, clientId) => {
89
90
  }
90
91
  return tokenData;
91
92
  };
92
- const login = async (options = {}) => {
93
+ const login = async () => {
93
94
  const { port, waitForCallback, shutdown } = await (0, server_1.startCallbackServer)();
94
95
  const redirectUri = `http://127.0.0.1:${port}`;
96
+ const promptAbort = new AbortController();
95
97
  try {
96
98
  const clientId = await registerClient(redirectUri);
97
99
  const { verifier, challenge } = generatePkce();
@@ -105,21 +107,24 @@ const login = async (options = {}) => {
105
107
  authorizeUrl.searchParams.set('code_challenge', challenge);
106
108
  authorizeUrl.searchParams.set('code_challenge_method', 'S256');
107
109
  const url = authorizeUrl.toString();
108
- options.onAuthorizeUrl?.(url);
109
- const isWindows = process.platform === 'win32';
110
- const escapedUrl = isWindows ? url.replace(/&/g, '^&') : url;
111
- const [command, ...arguments_] = isWindows
112
- ? ['start', '', escapedUrl]
113
- : process.platform === 'darwin'
114
- ? ['open', url]
115
- : ['xdg-open', url];
116
- (0, node_child_process_1.spawn)(command, arguments_, { stdio: 'ignore', shell: isWindows });
117
- const callback = await waitForCallback();
118
- if (callback.state !== state) {
110
+ await (0, url_1.openUrlInBrowser)(url);
111
+ const callback = waitForCallback();
112
+ const retryBrowserOnEnter = async () => {
113
+ if (await (0, stdin_1.promptToOpenAuthorizeUrl)(url, promptAbort.signal)) {
114
+ await (0, url_1.openUrlInBrowser)(url);
115
+ }
116
+ return callback;
117
+ };
118
+ const receivedCallback = await Promise.race([
119
+ callback,
120
+ retryBrowserOnEnter(),
121
+ ]);
122
+ promptAbort.abort();
123
+ if (receivedCallback.state !== state) {
119
124
  throw new Error('State mismatch. Possible CSRF attack.');
120
125
  }
121
126
  const tokenResponse = await exchangeCode({
122
- code: callback.code,
127
+ code: receivedCallback.code,
123
128
  redirectUri,
124
129
  clientId,
125
130
  codeVerifier: verifier,
@@ -128,9 +133,9 @@ const login = async (options = {}) => {
128
133
  (0, tokenStorage_1.saveCredentials)(tokens);
129
134
  return tokens;
130
135
  }
131
- catch (error) {
136
+ finally {
137
+ promptAbort.abort();
132
138
  shutdown();
133
- throw error;
134
139
  }
135
140
  };
136
141
  exports.login = login;
@@ -57,17 +57,9 @@ const startCallbackServer = () => new Promise((resolve, reject) => {
57
57
  response.end(SUCCESS_HTML);
58
58
  callbackResolve({ code, state });
59
59
  });
60
- const timeout = setTimeout(() => {
61
- shutdown();
62
- callbackReject(new Error('Authentication timed out. Please try again.'));
63
- }, constants_1.CALLBACK_TIMEOUT_MILLISECONDS);
64
- let isShutdown = false;
65
60
  const shutdown = () => {
66
- if (isShutdown)
67
- return;
68
- isShutdown = true;
69
- clearTimeout(timeout);
70
- server.close();
61
+ if (server.listening)
62
+ server.close();
71
63
  };
72
64
  server.listen(0, '127.0.0.1', () => {
73
65
  const address = server.address();
@@ -78,6 +70,11 @@ const startCallbackServer = () => new Promise((resolve, reject) => {
78
70
  resolve({
79
71
  port: address.port,
80
72
  waitForCallback: async () => {
73
+ const timeout = setTimeout(() => {
74
+ shutdown();
75
+ callbackReject(new Error('Authentication timed out. Please try again.'));
76
+ }, constants_1.CALLBACK_TIMEOUT_MILLISECONDS);
77
+ server.once('close', () => clearTimeout(timeout));
81
78
  const result = await callbackPromise;
82
79
  shutdown();
83
80
  return result;
@@ -13,24 +13,6 @@ const tokenStorage_1 = require("../auth/tokenStorage");
13
13
  const constants_1 = require("../constants");
14
14
  const output_1 = require("../helpers/output");
15
15
  const types_1 = require("../types");
16
- const AUTHORIZATION_URL_FALLBACK_DELAY_MILLISECONDS = 500;
17
- const getOpenUrlInstruction = () => process.platform === 'darwin' ? '⌘-click' : 'Ctrl-click';
18
- const createAuthorizationUrlFallback = () => {
19
- const abortController = new AbortController();
20
- const schedule = (url) => {
21
- const timeout = setTimeout(() => {
22
- if (abortController.signal.aborted)
23
- return;
24
- (0, output_1.printText)(`Browser didn't open? ${getOpenUrlInstruction()} the URL below to log in.\n`, output_1.TextType.Default);
25
- (0, output_1.printText)(chalk_1.default.cyan(url), output_1.TextType.Default);
26
- }, AUTHORIZATION_URL_FALLBACK_DELAY_MILLISECONDS);
27
- abortController.signal.addEventListener('abort', () => clearTimeout(timeout), { once: true });
28
- };
29
- return {
30
- schedule,
31
- cleanup: () => abortController.abort(),
32
- };
33
- };
34
16
  const loginCommand = new commander_1.Command('login')
35
17
  .description('Authenticate with Circleback.')
36
18
  .option('--api-key', 'Authenticate with a Public API key')
@@ -54,16 +36,8 @@ const loginCommand = new commander_1.Command('login')
54
36
  (0, output_1.printText)('API key authentication successful.', output_1.TextType.Success);
55
37
  return;
56
38
  }
57
- (0, output_1.printText)('Opening your browser for authentication…\n', output_1.TextType.Default);
58
- const authorizationUrlFallback = createAuthorizationUrlFallback();
59
- try {
60
- await (0, oauth_1.login)({
61
- onAuthorizeUrl: authorizationUrlFallback.schedule,
62
- });
63
- }
64
- finally {
65
- authorizationUrlFallback.cleanup();
66
- }
39
+ (0, output_1.printText)('Opening your browser for authentication…', output_1.TextType.Default);
40
+ await (0, oauth_1.login)();
67
41
  (0, output_1.printText)('Authentication successful.', output_1.TextType.Success);
68
42
  }
69
43
  catch (error) {
@@ -64,11 +64,12 @@ exports.searchMeetingsCommand = new commander_1.Command('search')
64
64
  });
65
65
  exports.readMeetingsCommand = new commander_1.Command('read')
66
66
  .description('Get detailed meeting information.')
67
- .argument('<ids...>', 'Meeting IDs (max 50)')
67
+ .argument('<ids...>', `Meeting IDs (max ${constants_1.MAX_READ_MEETING_IDENTIFIERS})`)
68
68
  .action(async (ids) => {
69
69
  try {
70
70
  const jsonMode = exports.readMeetingsCommand.parent?.parent?.opts().json ?? false;
71
71
  const meetingIds = (0, parse_1.validateMeetingIdentifiers)(ids);
72
+ (0, parse_1.validateReadMeetingIdentifierLimit)(meetingIds);
72
73
  const result = await (0, jsonRpc_1.callTool)('ReadMeetings', {
73
74
  intent: `Reading meeting details for IDs: ${meetingIds.join(', ')}.`,
74
75
  meetingIds,
package/dist/constants.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.READ_FAILED_ERROR = exports.SEARCH_FAILED_ERROR = exports.LAST_AND_FROM_CONFLICT_ERROR = exports.DOMAINS_OPTION_DESCRIPTION = exports.PROFILES_OPTION_DESCRIPTION = exports.TAGS_OPTION_DESCRIPTION = exports.END_DATE_OPTION_DESCRIPTION = exports.START_DATE_OPTION_DESCRIPTION = exports.PAGE_OPTION_DESCRIPTION = exports.UNASSIGNED_ACTION_ITEM_ASSIGNEE_LABEL = exports.DEFAULT_MEETING_NAME = exports.CALLBACK_TIMEOUT_MILLISECONDS = exports.OAUTH_SCOPE = exports.TOKENS_FILE = exports.CONFIG_DIRECTORY = exports.ASCII_LOGO = exports.SECONDARY_COLOR = exports.BRAND_COLOR = exports.API_KEY_SETTINGS_URL = exports.API_KEY_ENVIRONMENT_VARIABLE = exports.API_KEY_PREFIX = exports.CLIENT_VERSION_HEADER = exports.CLIENT_VERSION = exports.CLIENT_PACKAGE_NAME = exports.CLIENT_NAME = exports.BASE_URL = void 0;
6
+ exports.READ_FAILED_ERROR = exports.SEARCH_FAILED_ERROR = exports.LAST_AND_FROM_CONFLICT_ERROR = exports.DOMAINS_OPTION_DESCRIPTION = exports.PROFILES_OPTION_DESCRIPTION = exports.TAGS_OPTION_DESCRIPTION = exports.END_DATE_OPTION_DESCRIPTION = exports.START_DATE_OPTION_DESCRIPTION = exports.PAGE_OPTION_DESCRIPTION = exports.UNASSIGNED_ACTION_ITEM_ASSIGNEE_LABEL = exports.DEFAULT_MEETING_NAME = exports.MAX_READ_MEETING_IDENTIFIERS = exports.CALLBACK_TIMEOUT_MILLISECONDS = exports.OAUTH_SCOPE = exports.TOKENS_FILE = exports.CONFIG_DIRECTORY = exports.ASCII_LOGO = exports.SECONDARY_COLOR = exports.BRAND_COLOR = exports.API_KEY_SETTINGS_URL = exports.API_KEY_ENVIRONMENT_VARIABLE = exports.API_KEY_PREFIX = exports.CLIENT_VERSION_HEADER = exports.CLIENT_VERSION = exports.CLIENT_PACKAGE_NAME = exports.CLIENT_NAME = exports.BASE_URL = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const node_os_1 = __importDefault(require("node:os"));
@@ -45,6 +45,7 @@ exports.CONFIG_DIRECTORY = node_path_1.default.join(node_os_1.default.homedir(),
45
45
  exports.TOKENS_FILE = node_path_1.default.join(exports.CONFIG_DIRECTORY, 'tokens.json');
46
46
  exports.OAUTH_SCOPE = 'user';
47
47
  exports.CALLBACK_TIMEOUT_MILLISECONDS = 120_000;
48
+ exports.MAX_READ_MEETING_IDENTIFIERS = 50;
48
49
  exports.DEFAULT_MEETING_NAME = 'New meeting';
49
50
  exports.UNASSIGNED_ACTION_ITEM_ASSIGNEE_LABEL = 'Unassigned';
50
51
  exports.PAGE_OPTION_DESCRIPTION = 'Page index (starts at 0)';
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateMeetingIdentifiers = exports.parseNumericIds = exports.parseIntOption = exports.extractAndValidateObjectFromString = void 0;
3
+ exports.validateReadMeetingIdentifierLimit = exports.validateMeetingIdentifiers = exports.parseNumericIds = exports.parseIntOption = exports.extractAndValidateObjectFromString = void 0;
4
4
  const zod_1 = require("zod");
5
+ const constants_1 = require("../constants");
5
6
  const output_1 = require("./output");
6
7
  const extractAndValidateObjectFromString = (rawText, schema) => {
7
8
  try {
@@ -48,3 +49,10 @@ const validateMeetingIdentifiers = (values) => {
48
49
  return cleanedValues;
49
50
  };
50
51
  exports.validateMeetingIdentifiers = validateMeetingIdentifiers;
52
+ const validateReadMeetingIdentifierLimit = (ids) => {
53
+ if (ids.length > constants_1.MAX_READ_MEETING_IDENTIFIERS) {
54
+ (0, output_1.printText)(`Too many meeting identifiers. Expected at most ${constants_1.MAX_READ_MEETING_IDENTIFIERS}.`, output_1.TextType.Error);
55
+ process.exit(1);
56
+ }
57
+ };
58
+ exports.validateReadMeetingIdentifierLimit = validateReadMeetingIdentifierLimit;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.promptToOpenAuthorizeUrl = void 0;
7
+ const node_readline_1 = require("node:readline");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const output_1 = require("./output");
10
+ const string_1 = require("./string");
11
+ const promptToOpenAuthorizeUrl = async (url, signal, stdin = process.stdin) => {
12
+ const isInteractive = Boolean(stdin.isTTY && process.stdout.isTTY);
13
+ if (isInteractive) {
14
+ (0, output_1.printText)(`Browser didn't open? Press Enter to log in…`, output_1.TextType.Default);
15
+ (0, output_1.printText)('', output_1.TextType.Default);
16
+ }
17
+ (0, output_1.printText)(process.stdout.isTTY ? (0, string_1.link)(chalk_1.default.cyan.underline(url), url) : url, output_1.TextType.Default);
18
+ if (!isInteractive || signal?.aborted)
19
+ return false;
20
+ const readlineInterface = (0, node_readline_1.createInterface)({
21
+ input: stdin,
22
+ terminal: false,
23
+ signal,
24
+ });
25
+ return new Promise((resolve, reject) => {
26
+ readlineInterface.once('line', () => {
27
+ resolve(true);
28
+ readlineInterface.close();
29
+ });
30
+ readlineInterface.once('close', () => {
31
+ if (signal?.aborted) {
32
+ resolve(false);
33
+ return;
34
+ }
35
+ reject(new Error('Authentication canceled.'));
36
+ });
37
+ });
38
+ };
39
+ exports.promptToOpenAuthorizeUrl = promptToOpenAuthorizeUrl;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openUrlInBrowser = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_events_1 = require("node:events");
6
+ const output_1 = require("./output");
7
+ const openUrlInBrowser = async (url) => {
8
+ const isWindows = process.platform === 'win32';
9
+ const escapedUrl = isWindows ? url.replace(/&/g, '^&') : url;
10
+ const [command, ...arguments_] = isWindows
11
+ ? ['start', '', escapedUrl]
12
+ : process.platform === 'darwin'
13
+ ? ['open', url]
14
+ : ['xdg-open', url];
15
+ const warnAboutBrowserFailure = () => (0, output_1.printTextToStderr)('Unable to open your browser. Open the URL manually.', output_1.TextType.Warning);
16
+ try {
17
+ const child = (0, node_child_process_1.spawn)(command, arguments_, {
18
+ stdio: 'ignore',
19
+ shell: isWindows,
20
+ });
21
+ child.once('spawn', () => {
22
+ child.once('exit', (code) => {
23
+ if (code !== 0)
24
+ warnAboutBrowserFailure();
25
+ });
26
+ });
27
+ await (0, node_events_1.once)(child, 'spawn');
28
+ }
29
+ catch {
30
+ warnAboutBrowserFailure();
31
+ }
32
+ };
33
+ exports.openUrlInBrowser = openUrlInBrowser;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circleback/cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Circleback CLI. Search and access meetings, emails, calendar events, and more.",
5
5
  "license": "MIT",
6
6
  "author": "Circleback <support@circleback.ai> (https://circleback.ai)",