@verentis/cli 0.2.0 → 0.2.2
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/LICENSE +21 -0
- package/README.md +6 -0
- package/dist/index.js +141 -64
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Verentis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -24,6 +24,12 @@ verentis login --api-url … --token <identity token>
|
|
|
24
24
|
# VERENTIS_API_URL / VERENTIS_API_KEY / VERENTIS_TOKEN env vars also work
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
A device sign-in also stores a **refresh token**, so you stay signed in: when the 1-hour identity token
|
|
28
|
+
expires the CLI silently renews it on the next command (refresh tokens rotate on every use with reuse
|
|
29
|
+
detection). You only re-run `verentis login` after the refresh token lapses (~30 days idle) or `verentis
|
|
30
|
+
logout`. Tokens live in `~/.verentis/config.json` (mode `600`). Env-provided `VERENTIS_TOKEN` and API keys
|
|
31
|
+
are used as-is and never auto-refreshed.
|
|
32
|
+
|
|
27
33
|
Set defaults once so you can stop repeating flags:
|
|
28
34
|
|
|
29
35
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,87 @@ function isExpired(token, skewSeconds = 60) {
|
|
|
50
50
|
return payload.exp * 1e3 <= Date.now() + skewSeconds * 1e3;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// src/auth/deviceFlow.ts
|
|
54
|
+
var CLI_CLIENT_ID = "Verentis.Cli";
|
|
55
|
+
async function startDeviceAuthorization(apiUrl) {
|
|
56
|
+
const res = await fetch(`${apiUrl}/connect/device/authorize`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
59
|
+
body: new URLSearchParams({ client_id: CLI_CLIENT_ID })
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const text = await res.text().catch(() => "");
|
|
63
|
+
throw new Error(`Could not start device sign-in (${res.status}).${text ? ` ${text.slice(0, 300)}` : ""}`);
|
|
64
|
+
}
|
|
65
|
+
return await res.json();
|
|
66
|
+
}
|
|
67
|
+
async function pollForIdentityToken(apiUrl, authorization, onPoll) {
|
|
68
|
+
let intervalMs = Math.max(authorization.interval, 1) * 1e3;
|
|
69
|
+
const deadline = Date.now() + authorization.expires_in * 1e3;
|
|
70
|
+
while (Date.now() < deadline) {
|
|
71
|
+
await sleep(intervalMs);
|
|
72
|
+
onPoll?.();
|
|
73
|
+
const res = await fetch(`${apiUrl}/connect/token`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
76
|
+
body: new URLSearchParams({
|
|
77
|
+
grant_type: "device_code",
|
|
78
|
+
device_code: authorization.device_code,
|
|
79
|
+
client_id: CLI_CLIENT_ID
|
|
80
|
+
})
|
|
81
|
+
});
|
|
82
|
+
let payload;
|
|
83
|
+
try {
|
|
84
|
+
payload = await res.json();
|
|
85
|
+
} catch {
|
|
86
|
+
payload = void 0;
|
|
87
|
+
}
|
|
88
|
+
if (res.ok && payload?.id_token) {
|
|
89
|
+
return { idToken: payload.id_token, refreshToken: payload.refresh_token };
|
|
90
|
+
}
|
|
91
|
+
switch (payload?.error) {
|
|
92
|
+
case "authorization_pending":
|
|
93
|
+
continue;
|
|
94
|
+
case "slow_down":
|
|
95
|
+
intervalMs += 5e3;
|
|
96
|
+
continue;
|
|
97
|
+
case "access_denied":
|
|
98
|
+
throw new Error("Sign-in was denied in the browser.");
|
|
99
|
+
case "expired_token":
|
|
100
|
+
throw new Error("The device code expired before it was approved. Run `verentis login` again.");
|
|
101
|
+
default:
|
|
102
|
+
throw new Error(`Device sign-in failed (${res.status}).${payload?.error ? ` ${payload.error}` : ""}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
throw new Error("The device code expired before it was approved. Run `verentis login` again.");
|
|
106
|
+
}
|
|
107
|
+
var RefreshTokenError = class extends Error {
|
|
108
|
+
};
|
|
109
|
+
async function refreshIdentityToken(apiUrl, refreshToken) {
|
|
110
|
+
const res = await fetch(`${apiUrl}/connect/token`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
113
|
+
body: new URLSearchParams({
|
|
114
|
+
grant_type: "refresh_token",
|
|
115
|
+
refresh_token: refreshToken,
|
|
116
|
+
client_id: CLI_CLIENT_ID
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
let payload;
|
|
120
|
+
try {
|
|
121
|
+
payload = await res.json();
|
|
122
|
+
} catch {
|
|
123
|
+
payload = void 0;
|
|
124
|
+
}
|
|
125
|
+
if (res.ok && payload?.id_token) {
|
|
126
|
+
return { idToken: payload.id_token, refreshToken: payload.refresh_token ?? refreshToken };
|
|
127
|
+
}
|
|
128
|
+
throw new RefreshTokenError(
|
|
129
|
+
`Session could not be renewed${payload?.error ? ` (${payload.error})` : ` (${res.status})`}. Run \`verentis login\` again.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
var sleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
133
|
+
|
|
53
134
|
// src/http.ts
|
|
54
135
|
var ApiError = class extends Error {
|
|
55
136
|
constructor(status, message, body) {
|
|
@@ -65,6 +146,50 @@ async function createApiClient(overrides) {
|
|
|
65
146
|
const config = { ...await loadConfig(), ...overrides };
|
|
66
147
|
const apiUrl = requireApiUrl(config);
|
|
67
148
|
const tokenCache = /* @__PURE__ */ new Map();
|
|
149
|
+
const envIdentity = process.env.VERENTIS_TOKEN;
|
|
150
|
+
let currentIdentity = envIdentity ?? config.identityToken;
|
|
151
|
+
let currentRefresh = config.refreshToken;
|
|
152
|
+
let refreshInflight = null;
|
|
153
|
+
async function persistTokens(idToken, refreshToken) {
|
|
154
|
+
const disk = await loadConfig();
|
|
155
|
+
disk.identityToken = idToken;
|
|
156
|
+
if (refreshToken) disk.refreshToken = refreshToken;
|
|
157
|
+
await saveConfig(disk);
|
|
158
|
+
}
|
|
159
|
+
async function clearTokens() {
|
|
160
|
+
const disk = await loadConfig();
|
|
161
|
+
delete disk.identityToken;
|
|
162
|
+
delete disk.refreshToken;
|
|
163
|
+
await saveConfig(disk);
|
|
164
|
+
}
|
|
165
|
+
async function resolveIdentityToken() {
|
|
166
|
+
if (!currentIdentity) throw new ApiError(401, "Not logged in. Run `verentis login`.");
|
|
167
|
+
if (!isExpired(currentIdentity, 120)) return currentIdentity;
|
|
168
|
+
if (envIdentity || !currentRefresh) {
|
|
169
|
+
throw new ApiError(401, "Your session has expired \u2014 run `verentis login` again.");
|
|
170
|
+
}
|
|
171
|
+
if (!refreshInflight) {
|
|
172
|
+
refreshInflight = (async () => {
|
|
173
|
+
try {
|
|
174
|
+
const refreshed = await refreshIdentityToken(apiUrl, currentRefresh);
|
|
175
|
+
currentIdentity = refreshed.idToken;
|
|
176
|
+
currentRefresh = refreshed.refreshToken ?? currentRefresh;
|
|
177
|
+
await persistTokens(refreshed.idToken, refreshed.refreshToken);
|
|
178
|
+
return refreshed.idToken;
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (err instanceof RefreshTokenError) {
|
|
181
|
+
await clearTokens().catch(() => {
|
|
182
|
+
});
|
|
183
|
+
throw new ApiError(401, err.message);
|
|
184
|
+
}
|
|
185
|
+
throw err;
|
|
186
|
+
} finally {
|
|
187
|
+
refreshInflight = null;
|
|
188
|
+
}
|
|
189
|
+
})();
|
|
190
|
+
}
|
|
191
|
+
return refreshInflight;
|
|
192
|
+
}
|
|
68
193
|
async function credentialFor(scopes, workspaceId) {
|
|
69
194
|
const cacheKey = `${workspaceId ?? ""}:${[...scopes].sort().join(" ")}`;
|
|
70
195
|
const cached = tokenCache.get(cacheKey);
|
|
@@ -75,14 +200,10 @@ async function createApiClient(overrides) {
|
|
|
75
200
|
}
|
|
76
201
|
async function mintAccessToken(options) {
|
|
77
202
|
const apiKey = process.env.VERENTIS_API_KEY ?? config.apiKey;
|
|
78
|
-
|
|
79
|
-
const credential = apiKey ?? identityToken;
|
|
80
|
-
if (!credential) {
|
|
203
|
+
if (!apiKey && !currentIdentity) {
|
|
81
204
|
throw new Error("Not logged in. Run `verentis login` (or pass --api-key / --token).");
|
|
82
205
|
}
|
|
83
|
-
|
|
84
|
-
throw new ApiError(401, "Your identity token has expired \u2014 run `verentis login` again.");
|
|
85
|
-
}
|
|
206
|
+
const credential = apiKey ?? await resolveIdentityToken();
|
|
86
207
|
const res = await fetch(`${apiUrl}/v1/tokens/access`, {
|
|
87
208
|
method: "POST",
|
|
88
209
|
headers: {
|
|
@@ -1576,60 +1697,6 @@ function registerKeygen(program2) {
|
|
|
1576
1697
|
});
|
|
1577
1698
|
}
|
|
1578
1699
|
|
|
1579
|
-
// src/auth/deviceFlow.ts
|
|
1580
|
-
var CLI_CLIENT_ID = "Verentis.Cli";
|
|
1581
|
-
async function startDeviceAuthorization(apiUrl) {
|
|
1582
|
-
const res = await fetch(`${apiUrl}/connect/device/authorize`, {
|
|
1583
|
-
method: "POST",
|
|
1584
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1585
|
-
body: new URLSearchParams({ client_id: CLI_CLIENT_ID })
|
|
1586
|
-
});
|
|
1587
|
-
if (!res.ok) {
|
|
1588
|
-
const text = await res.text().catch(() => "");
|
|
1589
|
-
throw new Error(`Could not start device sign-in (${res.status}).${text ? ` ${text.slice(0, 300)}` : ""}`);
|
|
1590
|
-
}
|
|
1591
|
-
return await res.json();
|
|
1592
|
-
}
|
|
1593
|
-
async function pollForIdentityToken(apiUrl, authorization, onPoll) {
|
|
1594
|
-
let intervalMs = Math.max(authorization.interval, 1) * 1e3;
|
|
1595
|
-
const deadline = Date.now() + authorization.expires_in * 1e3;
|
|
1596
|
-
while (Date.now() < deadline) {
|
|
1597
|
-
await sleep(intervalMs);
|
|
1598
|
-
onPoll?.();
|
|
1599
|
-
const res = await fetch(`${apiUrl}/connect/token`, {
|
|
1600
|
-
method: "POST",
|
|
1601
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1602
|
-
body: new URLSearchParams({
|
|
1603
|
-
grant_type: "device_code",
|
|
1604
|
-
device_code: authorization.device_code,
|
|
1605
|
-
client_id: CLI_CLIENT_ID
|
|
1606
|
-
})
|
|
1607
|
-
});
|
|
1608
|
-
let payload;
|
|
1609
|
-
try {
|
|
1610
|
-
payload = await res.json();
|
|
1611
|
-
} catch {
|
|
1612
|
-
payload = void 0;
|
|
1613
|
-
}
|
|
1614
|
-
if (res.ok && payload?.id_token) return payload.id_token;
|
|
1615
|
-
switch (payload?.error) {
|
|
1616
|
-
case "authorization_pending":
|
|
1617
|
-
continue;
|
|
1618
|
-
case "slow_down":
|
|
1619
|
-
intervalMs += 5e3;
|
|
1620
|
-
continue;
|
|
1621
|
-
case "access_denied":
|
|
1622
|
-
throw new Error("Sign-in was denied in the browser.");
|
|
1623
|
-
case "expired_token":
|
|
1624
|
-
throw new Error("The device code expired before it was approved. Run `verentis login` again.");
|
|
1625
|
-
default:
|
|
1626
|
-
throw new Error(`Device sign-in failed (${res.status}).${payload?.error ? ` ${payload.error}` : ""}`);
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
throw new Error("The device code expired before it was approved. Run `verentis login` again.");
|
|
1630
|
-
}
|
|
1631
|
-
var sleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
1632
|
-
|
|
1633
1700
|
// src/commands/login.ts
|
|
1634
1701
|
function registerLogin(program2) {
|
|
1635
1702
|
program2.command("login").description("Sign in to Verentis (browser device flow by default; --api-key/--token for non-interactive use)").option("--api-url <url>", "Verentis API gateway URL").option("--api-key <key>", "Verentis API key (vrt_...)").option("--token <token>", "Verentis identity token (exchanged per request for scoped access tokens)").option("--sealing-key <key>", "the platform's base64 X25519 sealing PUBLIC key (used by `pack --encrypt`)").action(async (options) => {
|
|
@@ -1641,6 +1708,7 @@ function registerLogin(program2) {
|
|
|
1641
1708
|
if (!options.apiKey.startsWith("vrt_")) throw new Error('API keys start with "vrt_".');
|
|
1642
1709
|
config.apiKey = options.apiKey;
|
|
1643
1710
|
delete config.identityToken;
|
|
1711
|
+
delete config.refreshToken;
|
|
1644
1712
|
await saveConfig(config);
|
|
1645
1713
|
console.log(`Configuration saved (API key @ ${config.apiUrl}).`);
|
|
1646
1714
|
return;
|
|
@@ -1648,6 +1716,7 @@ function registerLogin(program2) {
|
|
|
1648
1716
|
if (options.token) {
|
|
1649
1717
|
config.identityToken = options.token;
|
|
1650
1718
|
delete config.apiKey;
|
|
1719
|
+
delete config.refreshToken;
|
|
1651
1720
|
await saveConfig(config);
|
|
1652
1721
|
console.log(`Configuration saved (identity token @ ${config.apiUrl}).`);
|
|
1653
1722
|
return;
|
|
@@ -1660,12 +1729,14 @@ function registerLogin(program2) {
|
|
|
1660
1729
|
`);
|
|
1661
1730
|
console.log(`and confirm the code: ${authorization.user_code}`);
|
|
1662
1731
|
console.log("\nWaiting for approval\u2026");
|
|
1663
|
-
const
|
|
1732
|
+
const identity = await pollForIdentityToken(apiUrl, authorization, () => process.stderr.write("."));
|
|
1664
1733
|
process.stderr.write("\n");
|
|
1665
|
-
config.identityToken =
|
|
1734
|
+
config.identityToken = identity.idToken;
|
|
1735
|
+
if (identity.refreshToken) config.refreshToken = identity.refreshToken;
|
|
1736
|
+
else delete config.refreshToken;
|
|
1666
1737
|
delete config.apiKey;
|
|
1667
1738
|
await saveConfig(config);
|
|
1668
|
-
const claims = decodeJwtPayload(
|
|
1739
|
+
const claims = decodeJwtPayload(identity.idToken);
|
|
1669
1740
|
console.log(`Signed in as ${claims?.name ?? claims?.email ?? claims?.sub ?? "unknown"} @ ${config.apiUrl}.`);
|
|
1670
1741
|
if (!claims?.account_id) {
|
|
1671
1742
|
console.log("Note: no unambiguous account context \u2014 admin commands may need --account <id>.");
|
|
@@ -1675,6 +1746,7 @@ function registerLogin(program2) {
|
|
|
1675
1746
|
const config = await loadConfig();
|
|
1676
1747
|
delete config.apiKey;
|
|
1677
1748
|
delete config.identityToken;
|
|
1749
|
+
delete config.refreshToken;
|
|
1678
1750
|
await saveConfig(config);
|
|
1679
1751
|
console.log("Credentials removed.");
|
|
1680
1752
|
});
|
|
@@ -1686,9 +1758,14 @@ function registerLogin(program2) {
|
|
|
1686
1758
|
if (claims) {
|
|
1687
1759
|
const expiry = claims.exp ? new Date(claims.exp * 1e3) : void 0;
|
|
1688
1760
|
const expired = expiry && expiry.getTime() <= Date.now();
|
|
1761
|
+
const renewable = Boolean(config.refreshToken) && !process.env.VERENTIS_TOKEN;
|
|
1689
1762
|
console.log(`User: ${claims.name ?? claims.email ?? claims.sub ?? "(unknown)"}${claims.sub ? ` (${claims.sub})` : ""}`);
|
|
1690
1763
|
if (claims.account_id) console.log(`Account: ${claims.account_id}`);
|
|
1691
|
-
if (expiry)
|
|
1764
|
+
if (expiry) {
|
|
1765
|
+
const state = expired ? renewable ? "expired (auto-renews)" : "EXPIRED" : "expires";
|
|
1766
|
+
console.log(`Token ${state}: ${expiry.toISOString()}`);
|
|
1767
|
+
}
|
|
1768
|
+
if (renewable) console.log("Session: refresh token stored (stays signed in across expiries)");
|
|
1692
1769
|
}
|
|
1693
1770
|
} else if (process.env.VERENTIS_API_KEY ?? config.apiKey) {
|
|
1694
1771
|
console.log("Credential: API key (identity claims not available)");
|