@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 +6 -0
- package/dist/index.js +190 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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: {
|
|
@@ -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
|
-
|
|
776
|
-
|
|
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
|
|
1767
|
+
const identity = await pollForIdentityToken(apiUrl, authorization, () => process.stderr.write("."));
|
|
1664
1768
|
process.stderr.write("\n");
|
|
1665
|
-
config.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(
|
|
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)
|
|
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);
|