@verentis/cli 0.2.1 → 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/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
- const identityToken = process.env.VERENTIS_TOKEN ?? config.identityToken;
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
- if (!apiKey && identityToken && isExpired(identityToken)) {
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 identityToken = await pollForIdentityToken(apiUrl, authorization, () => process.stderr.write("."));
1732
+ const identity = await pollForIdentityToken(apiUrl, authorization, () => process.stderr.write("."));
1664
1733
  process.stderr.write("\n");
1665
- config.identityToken = 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(identityToken);
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) console.log(`Token ${expired ? "EXPIRED" : "expires"}: ${expiry.toISOString()}`);
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)");