@faable/faable 1.5.23 β†’ 1.5.24

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.
@@ -5,8 +5,7 @@ const check_environment = async () => {
5
5
  await cmd("docker ps");
6
6
  }
7
7
  catch (error) {
8
- console.log(error);
9
- throw new Error(`Docker is not running`);
8
+ throw new Error(`Docker is not running`, { cause: error });
10
9
  }
11
10
  };
12
11
 
@@ -59,8 +59,14 @@ const deploy = {
59
59
  // Upload to Faable registry
60
60
  const { upload_tagname } = await upload_tag({ app, api });
61
61
  // Create a deployment for this image
62
- await api.createDeployment({ app_id: app.id, image: upload_tagname, type });
63
- log.info(`🌍 Deployment created -> https://${app.url}`);
62
+ const deployment = await api.createDeployment({
63
+ app_id: app.id,
64
+ image: upload_tagname,
65
+ type
66
+ });
67
+ const dashboard_url = `https://dashboard.faable.com/deploy/${app.team}/app/${app.id}`;
68
+ log.info(`🌍 Deployment created (${deployment.id}) -> https://${app.url}`);
69
+ log.info(`πŸ“Š View it in the dashboard -> ${dashboard_url}`);
64
70
  }
65
71
  };
66
72
 
@@ -1,4 +1,4 @@
1
- const strategy_docker = async (workdir) => {
1
+ const strategy_docker = async (_workdir) => {
2
2
  return {
3
3
  runtime: {
4
4
  name: "docker",
@@ -30,7 +30,7 @@ const strategy_nodejs = async (workdir) => {
30
30
  runtime_version = out.stdout.toString().trim();
31
31
  log.info(`Using node@${runtime_version} from engines in package.json (${engines.node})`);
32
32
  }
33
- catch (e) {
33
+ catch {
34
34
  log.info(`Node version defined in engines in package.json is not valid (${engines.node}), using current version ${runtime_version}`);
35
35
  }
36
36
  }
@@ -17,7 +17,7 @@ const getGitRemoteUrl = async (workdir) => {
17
17
  }
18
18
  return url;
19
19
  }
20
- catch (error) {
20
+ catch {
21
21
  log.warn("Could not detect git remote origin URL.");
22
22
  return undefined;
23
23
  }
@@ -1,9 +1,29 @@
1
1
  import { FaableApi } from '../../api/FaableApi.js';
2
2
  import { getDeviceCode, getDeviceToken } from '../../api/auth.js';
3
3
  import { CredentialsStore } from '../../lib/CredentialsStore.js';
4
+ import { bearer_strategy } from '../../api/strategies/bearer.strategy.js';
4
5
  import open from 'open';
6
+ import ora from 'ora';
5
7
  import { log } from '../../log.js';
6
8
 
9
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10
+ const renderUserCodeBlock = (code) => {
11
+ const padded = ` ${code} `;
12
+ const border = "─".repeat(padded.length);
13
+ return [
14
+ "",
15
+ ` β”Œ${border}┐`,
16
+ ` β”‚${padded}β”‚`,
17
+ ` β””${border}β”˜`,
18
+ "",
19
+ ].join("\n");
20
+ };
21
+ const extractOAuthError = (e) => {
22
+ const data = e?.response?.data || e?.cause?.response?.data || {};
23
+ if (typeof data === "object")
24
+ return data;
25
+ return {};
26
+ };
7
27
  const login = {
8
28
  command: "login",
9
29
  describe: "Login to Faable",
@@ -22,7 +42,6 @@ const login = {
22
42
  handler: async (args) => {
23
43
  const { apikey, token } = args;
24
44
  const store = new CredentialsStore();
25
- FaableApi.create(); // Base client for device flow
26
45
  if (apikey) {
27
46
  log.info("Logging in with API Key...");
28
47
  const tempApi = FaableApi.create({ auth: { apikey }, authStrategy: (await import('../../api/strategies/apikey.strategy.js')).apikey_strategy });
@@ -31,7 +50,7 @@ const login = {
31
50
  await store.saveCredentials({ apikey, email: me.email });
32
51
  log.info(`βœ… Successfully logged in as ${me.email}`);
33
52
  }
34
- catch (e) {
53
+ catch {
35
54
  log.error("❌ Invalid API Key");
36
55
  process.exit(1);
37
56
  }
@@ -45,33 +64,69 @@ const login = {
45
64
  await store.saveCredentials({ token, email: me.email });
46
65
  log.info(`βœ… Successfully logged in as ${me.email}`);
47
66
  }
48
- catch (e) {
67
+ catch {
49
68
  log.error("❌ Invalid OIDC token");
50
69
  process.exit(1);
51
70
  }
52
71
  return;
53
72
  }
54
- // Interactive Device Flow
73
+ // Interactive Device Authorization Grant (RFC 8628)
55
74
  log.info("Starting browser-based authentication...");
75
+ let device_code;
76
+ let user_code;
77
+ let verification_uri;
78
+ let verification_uri_complete;
79
+ let interval;
80
+ let expires_in;
56
81
  try {
57
- const { device_code, user_code, verification_uri, interval, expires_in } = await getDeviceCode();
58
- log.info(`\nVerification code: ${user_code}\n`);
59
- log.info(`If your browser doesn't open automatically, please visit:\n${verification_uri}\n`);
60
- try {
61
- await open(verification_uri);
62
- }
63
- catch (e) {
64
- log.warn("Could not open browser automatically.");
65
- }
66
- // Polling
67
- const start = Date.now();
68
- const timeout = expires_in * 1000;
69
- while (Date.now() - start < timeout) {
82
+ const dc = await getDeviceCode();
83
+ device_code = dc.device_code;
84
+ user_code = dc.user_code;
85
+ verification_uri = dc.verification_uri;
86
+ verification_uri_complete = dc.verification_uri_complete;
87
+ interval = dc.interval;
88
+ expires_in = dc.expires_in;
89
+ }
90
+ catch (e) {
91
+ log.error(`❌ Failed to start authentication: ${e.message}`);
92
+ process.exit(1);
93
+ }
94
+ process.stdout.write(renderUserCodeBlock(user_code));
95
+ log.info(`If your browser doesn't open automatically, visit: ${verification_uri}`);
96
+ try {
97
+ await open(verification_uri_complete);
98
+ }
99
+ catch {
100
+ log.warn("Could not open browser automatically.");
101
+ }
102
+ const spinner = ora({
103
+ text: "Waiting for confirmation in browser…",
104
+ spinner: "dots",
105
+ }).start();
106
+ let cancelled = false;
107
+ const onSigint = () => {
108
+ cancelled = true;
109
+ spinner.stop();
110
+ process.stderr.write("\nLogin cancelled.\n");
111
+ process.exit(130);
112
+ };
113
+ process.once("SIGINT", onSigint);
114
+ const start = Date.now();
115
+ const timeoutMs = expires_in * 1000;
116
+ let currentInterval = interval;
117
+ try {
118
+ while (!cancelled && Date.now() - start < timeoutMs) {
119
+ await wait(currentInterval * 1000);
120
+ if (cancelled)
121
+ return;
70
122
  try {
71
123
  const { access_token } = await getDeviceToken(device_code);
72
124
  if (access_token) {
73
- log.info("Token received!");
74
- const tempApi = FaableApi.create({ auth: { token: access_token }, authStrategy: () => ({ headers: async () => ({ Authorization: `Bearer ${access_token}` }) }) });
125
+ spinner.stop();
126
+ const tempApi = FaableApi.create({
127
+ auth: { token: access_token },
128
+ authStrategy: bearer_strategy,
129
+ });
75
130
  const me = await tempApi.getMe();
76
131
  await store.saveCredentials({ token: access_token, email: me.email });
77
132
  log.info(`βœ… Successfully logged in as ${me.email}`);
@@ -79,27 +134,38 @@ const login = {
79
134
  }
80
135
  }
81
136
  catch (e) {
82
- // Typically returns 400 with "authorization_pending"
83
- const errData = e.response?.data || e.cause?.response?.data;
84
- const errHeaders = e.response?.headers || e.cause?.response?.headers;
85
- if (errData?.error === "authorization_pending") {
86
- // Wait and continue
137
+ const { error, error_description } = extractOAuthError(e);
138
+ if (error === "authorization_pending") {
139
+ continue;
87
140
  }
88
- else if (errData?.error === "slow_down") {
89
- // Should increase interval but for now we just wait
141
+ if (error === "slow_down") {
142
+ currentInterval += 5;
143
+ spinner.text = `Waiting for confirmation in browser… (slowing polling to ${currentInterval}s)`;
144
+ continue;
90
145
  }
91
- else if (errHeaders?.["content-type"]?.includes("application/json")) {
92
- // Other error
146
+ if (error === "access_denied") {
147
+ spinner.stop();
148
+ log.error("❌ Authorization denied. Run `faable login` again to retry.");
149
+ process.exit(1);
93
150
  }
151
+ if (error === "expired_token") {
152
+ spinner.stop();
153
+ log.error("❌ Code expired. Run `faable login` again to start over.");
154
+ process.exit(1);
155
+ }
156
+ // Unknown OAuth error or non-OAuth failure
157
+ spinner.stop();
158
+ const detail = error_description || error || e?.message || "unknown error";
159
+ log.error(`❌ Authentication failed: ${detail}`);
160
+ process.exit(1);
94
161
  }
95
- await new Promise(resolve => setTimeout(resolve, interval * 1000));
96
162
  }
97
- log.error("❌ Authentication timed out");
163
+ spinner.stop();
164
+ log.error("❌ Code expired. Run `faable login` again to start over.");
98
165
  process.exit(1);
99
166
  }
100
- catch (e) {
101
- log.error(`❌ Failed to start authentication: ${e.message}`);
102
- process.exit(1);
167
+ finally {
168
+ process.removeListener("SIGINT", onSigint);
103
169
  }
104
170
  },
105
171
  };
@@ -14,7 +14,7 @@ const whoami = {
14
14
  const me = await api.getMe();
15
15
  log.info(`Logged in as: ${me.email}`);
16
16
  }
17
- catch (e) {
17
+ catch {
18
18
  log.error("❌ Not logged in or session expired");
19
19
  process.exit(1);
20
20
  }
package/dist/lib/cmd.js CHANGED
@@ -33,7 +33,7 @@ const cmd = async (cmd, config) => {
33
33
  if (output) {
34
34
  log.error(output);
35
35
  }
36
- throw new Error(`Command error: ${cmd}`);
36
+ throw new Error(`Command error: ${cmd}`, { cause: error });
37
37
  }
38
38
  };
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "1.5.23",
3
+ "version": "1.5.24",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",
@@ -31,6 +31,7 @@
31
31
  "fs-extra": "^11.3.2",
32
32
  "handlebars": "^4.7.8",
33
33
  "open": "^11.0.0",
34
+ "ora": "^9.4.0",
34
35
  "pino": "^10.1.0",
35
36
  "pino-pretty": "^13.1.3",
36
37
  "promisify-child-process": "^4.1.2",