@verentis/cli 0.2.1 → 0.2.3

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: {
@@ -772,14 +893,49 @@ function validateManifest(manifest) {
772
893
  if (!manifestPermissions(manifest).length) warning("spec.permissions is empty \u2014 the run token will carry no VFS scopes");
773
894
  }
774
895
  if (kind === "Application") {
775
- if (!spec.entry && !spec["entry-points"]?.length) {
776
- error("applications need spec.entry or spec.entry-points[]");
896
+ const hasIframe = !!spec.entry || !!spec["entry-points"]?.length;
897
+ const hasInstall = Array.isArray(spec.install) && spec.install.length > 0;
898
+ const hasScopes = Array.isArray(spec.scopes) && spec.scopes.length > 0;
899
+ if (!hasIframe && !hasInstall && !hasScopes) {
900
+ error(
901
+ "an Application must declare at least one contribution: spec.entry / spec.entry-points[] (iframe UI), spec.install[] (installed files), or spec.scopes[] (identity)"
902
+ );
777
903
  }
778
904
  }
905
+ validateInstallDirectives(spec.install, issues);
779
906
  const packaging = manifest.packaging ?? {};
780
907
  if (!packaging.publisher) warning("packaging.publisher is not set \u2014 `verentis pack`/`publish` will refuse the package");
781
908
  return issues;
782
909
  }
910
+ function validateInstallDirectives(install, issues) {
911
+ if (install === void 0) return;
912
+ const error = (message) => issues.push({ level: "error", message });
913
+ if (!Array.isArray(install)) {
914
+ error("spec.install must be a list of install directives");
915
+ return;
916
+ }
917
+ install.forEach((directive, index) => {
918
+ const at = `spec.install[${index}]`;
919
+ if (!directive || typeof directive !== "object") {
920
+ error(`${at} must be an object with 'from' and 'to'`);
921
+ return;
922
+ }
923
+ if (!directive.from || typeof directive.from !== "string") {
924
+ error(`${at}.from is required (a path/glob relative to the package payload)`);
925
+ } else if (directive.from.startsWith("/") || directive.from.split("/").includes("..")) {
926
+ error(`${at}.from must be a relative path inside the package payload (no leading '/' or '..')`);
927
+ }
928
+ if (!directive.to || typeof directive.to !== "string") {
929
+ error(`${at}.to is required (a workspace-absolute destination path)`);
930
+ } else {
931
+ if (!directive.to.startsWith("/")) error(`${at}.to must be a workspace-absolute path (start with '/')`);
932
+ if (directive.to.split("/").includes("..")) error(`${at}.to must not contain '..'`);
933
+ }
934
+ if (directive.mode !== void 0 && directive.mode !== "seed" && directive.mode !== "managed") {
935
+ error(`${at}.mode must be 'seed' or 'managed' (got '${directive.mode}')`);
936
+ }
937
+ });
938
+ }
783
939
  var KIND_MAP = {
784
940
  application: "Application",
785
941
  executionengine: "ExecutionEngine",
@@ -1576,60 +1732,6 @@ function registerKeygen(program2) {
1576
1732
  });
1577
1733
  }
1578
1734
 
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
1735
  // src/commands/login.ts
1634
1736
  function registerLogin(program2) {
1635
1737
  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 +1743,7 @@ function registerLogin(program2) {
1641
1743
  if (!options.apiKey.startsWith("vrt_")) throw new Error('API keys start with "vrt_".');
1642
1744
  config.apiKey = options.apiKey;
1643
1745
  delete config.identityToken;
1746
+ delete config.refreshToken;
1644
1747
  await saveConfig(config);
1645
1748
  console.log(`Configuration saved (API key @ ${config.apiUrl}).`);
1646
1749
  return;
@@ -1648,6 +1751,7 @@ function registerLogin(program2) {
1648
1751
  if (options.token) {
1649
1752
  config.identityToken = options.token;
1650
1753
  delete config.apiKey;
1754
+ delete config.refreshToken;
1651
1755
  await saveConfig(config);
1652
1756
  console.log(`Configuration saved (identity token @ ${config.apiUrl}).`);
1653
1757
  return;
@@ -1660,12 +1764,14 @@ function registerLogin(program2) {
1660
1764
  `);
1661
1765
  console.log(`and confirm the code: ${authorization.user_code}`);
1662
1766
  console.log("\nWaiting for approval\u2026");
1663
- const identityToken = await pollForIdentityToken(apiUrl, authorization, () => process.stderr.write("."));
1767
+ const identity = await pollForIdentityToken(apiUrl, authorization, () => process.stderr.write("."));
1664
1768
  process.stderr.write("\n");
1665
- config.identityToken = identityToken;
1769
+ config.identityToken = identity.idToken;
1770
+ if (identity.refreshToken) config.refreshToken = identity.refreshToken;
1771
+ else delete config.refreshToken;
1666
1772
  delete config.apiKey;
1667
1773
  await saveConfig(config);
1668
- const claims = decodeJwtPayload(identityToken);
1774
+ const claims = decodeJwtPayload(identity.idToken);
1669
1775
  console.log(`Signed in as ${claims?.name ?? claims?.email ?? claims?.sub ?? "unknown"} @ ${config.apiUrl}.`);
1670
1776
  if (!claims?.account_id) {
1671
1777
  console.log("Note: no unambiguous account context \u2014 admin commands may need --account <id>.");
@@ -1675,6 +1781,7 @@ function registerLogin(program2) {
1675
1781
  const config = await loadConfig();
1676
1782
  delete config.apiKey;
1677
1783
  delete config.identityToken;
1784
+ delete config.refreshToken;
1678
1785
  await saveConfig(config);
1679
1786
  console.log("Credentials removed.");
1680
1787
  });
@@ -1686,9 +1793,14 @@ function registerLogin(program2) {
1686
1793
  if (claims) {
1687
1794
  const expiry = claims.exp ? new Date(claims.exp * 1e3) : void 0;
1688
1795
  const expired = expiry && expiry.getTime() <= Date.now();
1796
+ const renewable = Boolean(config.refreshToken) && !process.env.VERENTIS_TOKEN;
1689
1797
  console.log(`User: ${claims.name ?? claims.email ?? claims.sub ?? "(unknown)"}${claims.sub ? ` (${claims.sub})` : ""}`);
1690
1798
  if (claims.account_id) console.log(`Account: ${claims.account_id}`);
1691
- if (expiry) console.log(`Token ${expired ? "EXPIRED" : "expires"}: ${expiry.toISOString()}`);
1799
+ if (expiry) {
1800
+ const state = expired ? renewable ? "expired (auto-renews)" : "EXPIRED" : "expires";
1801
+ console.log(`Token ${state}: ${expiry.toISOString()}`);
1802
+ }
1803
+ if (renewable) console.log("Session: refresh token stored (stays signed in across expiries)");
1692
1804
  }
1693
1805
  } else if (process.env.VERENTIS_API_KEY ?? config.apiKey) {
1694
1806
  console.log("Credential: API key (identity claims not available)");
@@ -1897,9 +2009,21 @@ async function resolvePayloadFiles(projectDir, manifestPath, manifest) {
1897
2009
  function defaultIncludes(manifest) {
1898
2010
  const sources = /* @__PURE__ */ new Set();
1899
2011
  collectSchemaSources(manifest, sources);
2012
+ collectInstallSources(manifest, sources);
1900
2013
  if (sources.size === 0) return [];
1901
2014
  return [...sources];
1902
2015
  }
2016
+ function collectInstallSources(manifest, sources) {
2017
+ const spec = manifest.spec;
2018
+ const install = spec?.install;
2019
+ if (!Array.isArray(install)) return;
2020
+ for (const directive of install) {
2021
+ const from = directive?.from;
2022
+ if (typeof from === "string" && from.length > 0 && !from.startsWith("/")) {
2023
+ sources.add(from);
2024
+ }
2025
+ }
2026
+ }
1903
2027
  function collectSchemaSources(node, sources) {
1904
2028
  if (Array.isArray(node)) {
1905
2029
  for (const item of node) collectSchemaSources(item, sources);