@faable/faable 1.6.0 → 1.7.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/dist/api/FaableApi.js +7 -0
- package/dist/api/auth.js +11 -1
- package/dist/api/context.js +17 -2
- package/dist/api/session.js +55 -0
- package/dist/commands/deploy/index.js +2 -6
- package/dist/commands/link/index.js +2 -6
- package/dist/commands/login/index.js +16 -4
- package/dist/commands/whoami/index.js +3 -1
- package/package.json +1 -1
package/dist/api/FaableApi.js
CHANGED
|
@@ -33,6 +33,13 @@ class FaableApi {
|
|
|
33
33
|
const res = e.response;
|
|
34
34
|
const url = e.config?.url || "";
|
|
35
35
|
if (res) {
|
|
36
|
+
// Uniform handling for an expired/invalid session across every
|
|
37
|
+
// command, regardless of which endpoint returned the 401.
|
|
38
|
+
if (res.status === 401) {
|
|
39
|
+
const expired = new Error("Your Faable session has expired or is invalid. Run `faable login` to sign in again.", { cause: error });
|
|
40
|
+
expired.status = 401;
|
|
41
|
+
throw expired;
|
|
42
|
+
}
|
|
36
43
|
const serverMessage = res.data?.message || res.statusText || "Unknown Error";
|
|
37
44
|
const wrapped = new Error(`FaableApi ${url} ${res.status}: ${serverMessage}`, { cause: error });
|
|
38
45
|
// Surface the structured error contract (e.g. the repository-link
|
package/dist/api/auth.js
CHANGED
|
@@ -35,5 +35,15 @@ async function getMe(access_token) {
|
|
|
35
35
|
});
|
|
36
36
|
return res.data;
|
|
37
37
|
}
|
|
38
|
+
// Exchange a refresh token for a fresh access token (and a rotated refresh
|
|
39
|
+
// token). The CLI is a public client, so no client_secret is required.
|
|
40
|
+
async function refreshToken(refresh_token) {
|
|
41
|
+
const res = await api.post(`/oauth/token`, {
|
|
42
|
+
grant_type: "refresh_token",
|
|
43
|
+
client_id: CLIENT_ID,
|
|
44
|
+
refresh_token,
|
|
45
|
+
});
|
|
46
|
+
return res.data;
|
|
47
|
+
}
|
|
38
48
|
|
|
39
|
-
export { getDeviceCode, getDeviceToken, getMe };
|
|
49
|
+
export { getDeviceCode, getDeviceToken, getMe, refreshToken };
|
package/dist/api/context.js
CHANGED
|
@@ -4,6 +4,8 @@ import { getIDToken } from '@actions/core';
|
|
|
4
4
|
import { oidc_strategy } from './strategies/oidc.strategy.js';
|
|
5
5
|
import { bearer_strategy } from './strategies/bearer.strategy.js';
|
|
6
6
|
import { CredentialsStore } from '../lib/CredentialsStore.js';
|
|
7
|
+
import { loadLiveCredentials } from './session.js';
|
|
8
|
+
import { log } from '../log.js';
|
|
7
9
|
|
|
8
10
|
const context = async () => {
|
|
9
11
|
let api;
|
|
@@ -25,7 +27,8 @@ const context = async () => {
|
|
|
25
27
|
}
|
|
26
28
|
else {
|
|
27
29
|
const store = new CredentialsStore();
|
|
28
|
-
|
|
30
|
+
// Auto-refreshes an expired access token via the stored refresh_token.
|
|
31
|
+
const config = await loadLiveCredentials(store);
|
|
29
32
|
if (config) {
|
|
30
33
|
if (config.token) {
|
|
31
34
|
api = FaableApi.create({
|
|
@@ -47,5 +50,17 @@ const context = async () => {
|
|
|
47
50
|
appId,
|
|
48
51
|
};
|
|
49
52
|
};
|
|
53
|
+
// Resolve the API client for a command that needs an authenticated session.
|
|
54
|
+
// Exits with a uniform message when there is no session at all. An expired
|
|
55
|
+
// session that could not be refreshed still surfaces here as a working `api`
|
|
56
|
+
// whose first call returns 401 — FaableApi maps that to the same re-login hint.
|
|
57
|
+
const requireApi = async () => {
|
|
58
|
+
const ctx = await context();
|
|
59
|
+
if (!ctx.api) {
|
|
60
|
+
log.error("❌ Not logged in. Run 'faable login' first.");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
return ctx;
|
|
64
|
+
};
|
|
50
65
|
|
|
51
|
-
export { context };
|
|
66
|
+
export { context, requireApi };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CredentialsStore } from '../lib/CredentialsStore.js';
|
|
2
|
+
import { refreshToken } from './auth.js';
|
|
3
|
+
import { log } from '../log.js';
|
|
4
|
+
|
|
5
|
+
// True when a stored JWT access token is still valid (has a future `exp`).
|
|
6
|
+
// Anything we can't decode, or without `exp`, counts as NOT live — callers then
|
|
7
|
+
// refresh or ask the user to re-login instead of trusting a dead token.
|
|
8
|
+
const isTokenLive = (token) => {
|
|
9
|
+
try {
|
|
10
|
+
const [, payload] = token.split(".");
|
|
11
|
+
const claims = JSON.parse(Buffer.from(payload, "base64url").toString());
|
|
12
|
+
if (typeof claims.exp !== "number")
|
|
13
|
+
return false;
|
|
14
|
+
// 30s skew so a near-expired token is treated as dead and refreshed early.
|
|
15
|
+
return claims.exp * 1000 > Date.now() + 30_000;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
// Load local credentials, transparently refreshing an expired (or near-expired)
|
|
22
|
+
// access token using the stored refresh_token. The refreshed credentials are
|
|
23
|
+
// persisted so the next command reuses them. Returns the (possibly refreshed)
|
|
24
|
+
// config, or undefined when there are no local credentials.
|
|
25
|
+
//
|
|
26
|
+
// This is the single place the CLI resolves a local session, so every command
|
|
27
|
+
// that goes through it gets auto-refresh for free. If the refresh fails (e.g.
|
|
28
|
+
// the refresh token itself expired or was revoked) we return the stale config
|
|
29
|
+
// and let the downstream 401 surface the uniform "session expired" error.
|
|
30
|
+
const loadLiveCredentials = async (store = new CredentialsStore()) => {
|
|
31
|
+
const config = await store.loadCredentials();
|
|
32
|
+
if (!config)
|
|
33
|
+
return undefined;
|
|
34
|
+
const needsRefresh = !!config.token && !isTokenLive(config.token);
|
|
35
|
+
if (needsRefresh && config.refresh_token) {
|
|
36
|
+
try {
|
|
37
|
+
const refreshed = await refreshToken(config.refresh_token);
|
|
38
|
+
const next = {
|
|
39
|
+
...config,
|
|
40
|
+
token: refreshed.access_token,
|
|
41
|
+
refresh_token: refreshed.refresh_token ?? config.refresh_token,
|
|
42
|
+
};
|
|
43
|
+
await store.saveCredentials(next);
|
|
44
|
+
log.debug?.("Refreshed access token");
|
|
45
|
+
return next;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Refresh failed — keep the stale config; the API call's 401 (or
|
|
49
|
+
// requireApi) will tell the user to run `faable login` again.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return config;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export { isTokenLive, loadLiveCredentials };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { requireApi } from '../../api/context.js';
|
|
2
2
|
import { cmd } from '../../lib/cmd.js';
|
|
3
3
|
import { Configuration } from '../../lib/Configuration.js';
|
|
4
4
|
import { log } from '../../log.js';
|
|
@@ -27,12 +27,8 @@ const deploy = {
|
|
|
27
27
|
},
|
|
28
28
|
handler: async (args) => {
|
|
29
29
|
const workdir = args.workdir || process.cwd();
|
|
30
|
-
const ctx = await
|
|
30
|
+
const ctx = await requireApi();
|
|
31
31
|
const { api } = ctx;
|
|
32
|
-
if (!api) {
|
|
33
|
-
log.error("❌ Not logged in. Run 'faable login' first.");
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
32
|
// Resolve runtime
|
|
37
33
|
const { runtime } = await runtime_detection(workdir);
|
|
38
34
|
// app_id resolution (the user never has to look one up):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { requireApi } from '../../api/context.js';
|
|
2
2
|
import prompts from 'prompts';
|
|
3
3
|
import { log } from '../../log.js';
|
|
4
4
|
import { cmd } from '../../lib/cmd.js';
|
|
@@ -60,11 +60,7 @@ const link = {
|
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
const { api } = await
|
|
64
|
-
if (!api) {
|
|
65
|
-
log.error("❌ Not logged in. Run 'faable login' first.");
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
63
|
+
const { api } = await requireApi();
|
|
68
64
|
log.info("Checking local git repository...");
|
|
69
65
|
const gitUrl = await getGitRemoteUrl(workdir);
|
|
70
66
|
const apps = await api.list();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FaableApi } from '../../api/FaableApi.js';
|
|
2
2
|
import { getDeviceCode, getDeviceToken, getMe } from '../../api/auth.js';
|
|
3
|
+
import { isTokenLive } from '../../api/session.js';
|
|
3
4
|
import { CredentialsStore } from '../../lib/CredentialsStore.js';
|
|
4
5
|
import open from 'open';
|
|
5
6
|
import ora from 'ora';
|
|
@@ -47,8 +48,13 @@ const login = {
|
|
|
47
48
|
// re-auth (and often scripted), so they skip the prompt and overwrite.
|
|
48
49
|
if (!apikey && !token) {
|
|
49
50
|
const existing = await store.loadCredentials();
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
// Only a still-valid bearer session (or an API key) counts as "logged
|
|
52
|
+
// in". An expired token is a dead session, so skip the prompt and go
|
|
53
|
+
// straight to re-login instead of misleadingly saying "already logged in".
|
|
54
|
+
const hasLiveSession = (!!existing?.token && isTokenLive(existing.token)) ||
|
|
55
|
+
!!existing?.apikey;
|
|
56
|
+
if (hasLiveSession) {
|
|
57
|
+
const who = existing?.email ? ` as ${existing.email}` : "";
|
|
52
58
|
const { proceed } = await prompts({
|
|
53
59
|
type: "confirm",
|
|
54
60
|
name: "proceed",
|
|
@@ -140,13 +146,19 @@ const login = {
|
|
|
140
146
|
if (cancelled)
|
|
141
147
|
return;
|
|
142
148
|
try {
|
|
143
|
-
const { access_token } = await getDeviceToken(device_code);
|
|
149
|
+
const { access_token, refresh_token } = await getDeviceToken(device_code);
|
|
144
150
|
if (access_token) {
|
|
145
151
|
spinner.stop();
|
|
146
152
|
// Validate the freshly issued token against the Auth server (it
|
|
147
153
|
// issued the token). The deploy API has no /me route.
|
|
148
154
|
const me = await getMe(access_token);
|
|
149
|
-
|
|
155
|
+
// Persist the refresh_token so the session can be renewed silently
|
|
156
|
+
// when the access token expires (see api/session.ts).
|
|
157
|
+
await store.saveCredentials({
|
|
158
|
+
token: access_token,
|
|
159
|
+
refresh_token,
|
|
160
|
+
email: me.email,
|
|
161
|
+
});
|
|
150
162
|
log.info(`✅ Successfully logged in as ${me.email}`);
|
|
151
163
|
return;
|
|
152
164
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { log } from '../../log.js';
|
|
2
2
|
import { getMe } from '../../api/auth.js';
|
|
3
|
+
import { loadLiveCredentials } from '../../api/session.js';
|
|
3
4
|
import { CredentialsStore } from '../../lib/CredentialsStore.js';
|
|
4
5
|
|
|
5
6
|
const whoami = {
|
|
@@ -7,7 +8,8 @@ const whoami = {
|
|
|
7
8
|
describe: "Display the current logged in user",
|
|
8
9
|
handler: async () => {
|
|
9
10
|
const store = new CredentialsStore();
|
|
10
|
-
|
|
11
|
+
// Auto-refreshes an expired token via the stored refresh_token.
|
|
12
|
+
const config = await loadLiveCredentials(store);
|
|
11
13
|
// Bearer token from the environment (CI) or a local `faable login`.
|
|
12
14
|
const token = process.env.FAABLE_TOKEN || config?.token;
|
|
13
15
|
if (!token) {
|