@geolonia/geonicdb-cli 0.4.0 → 0.5.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/README.md +93 -1
- package/dist/index.js +487 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -240,14 +240,34 @@ function collectKeys(items) {
|
|
|
240
240
|
sorted.push(...Array.from(keySet).sort());
|
|
241
241
|
return sorted;
|
|
242
242
|
}
|
|
243
|
+
function formatGeoJSON(geo) {
|
|
244
|
+
const geoType = String(geo.type);
|
|
245
|
+
const coords = geo.coordinates;
|
|
246
|
+
if (geoType === "Point" && Array.isArray(coords) && coords.length >= 2) {
|
|
247
|
+
return `Point(${Number(coords[0]).toFixed(2)}, ${Number(coords[1]).toFixed(2)})`;
|
|
248
|
+
}
|
|
249
|
+
if (Array.isArray(coords)) {
|
|
250
|
+
const count = geoType === "Polygon" ? Array.isArray(coords[0]) ? coords[0].length : 0 : coords.length;
|
|
251
|
+
return `${geoType}(${count} coords)`;
|
|
252
|
+
}
|
|
253
|
+
return `${geoType}(...)`;
|
|
254
|
+
}
|
|
255
|
+
function isGeoJSON(v) {
|
|
256
|
+
if (typeof v !== "object" || v === null) return false;
|
|
257
|
+
const o = v;
|
|
258
|
+
return typeof o.type === "string" && "coordinates" in o;
|
|
259
|
+
}
|
|
243
260
|
function cellValue(val) {
|
|
244
261
|
if (val === void 0 || val === null) return "";
|
|
245
|
-
if (typeof val
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
262
|
+
if (typeof val !== "object") return String(val);
|
|
263
|
+
const obj = val;
|
|
264
|
+
if (isGeoJSON(obj)) return formatGeoJSON(obj);
|
|
265
|
+
if ("value" in obj) {
|
|
266
|
+
const v = obj.value;
|
|
267
|
+
if (isGeoJSON(v)) return formatGeoJSON(v);
|
|
268
|
+
return String(v);
|
|
249
269
|
}
|
|
250
|
-
return
|
|
270
|
+
return JSON.stringify(val);
|
|
251
271
|
}
|
|
252
272
|
function toGeoJSON(data) {
|
|
253
273
|
if (Array.isArray(data)) {
|
|
@@ -582,6 +602,7 @@ function registerConfigCommand(program2) {
|
|
|
582
602
|
}
|
|
583
603
|
|
|
584
604
|
// src/commands/auth.ts
|
|
605
|
+
import { createHash } from "crypto";
|
|
585
606
|
import { Command } from "commander";
|
|
586
607
|
|
|
587
608
|
// src/oauth.ts
|
|
@@ -653,7 +674,7 @@ var GdbClient = class _GdbClient {
|
|
|
653
674
|
if (this.token) {
|
|
654
675
|
headers["Authorization"] = `Bearer ${this.token}`;
|
|
655
676
|
} else if (this.apiKey) {
|
|
656
|
-
headers["
|
|
677
|
+
headers["X-Api-Key"] = this.apiKey;
|
|
657
678
|
}
|
|
658
679
|
if (extra) {
|
|
659
680
|
Object.assign(headers, extra);
|
|
@@ -674,13 +695,15 @@ var GdbClient = class _GdbClient {
|
|
|
674
695
|
getBasePath() {
|
|
675
696
|
return "/ngsi-ld/v1";
|
|
676
697
|
}
|
|
677
|
-
static SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization"]);
|
|
698
|
+
static SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "x-api-key"]);
|
|
678
699
|
static SENSITIVE_BODY_KEYS = /* @__PURE__ */ new Set([
|
|
679
700
|
"password",
|
|
680
701
|
"refreshToken",
|
|
681
702
|
"token",
|
|
682
703
|
"client_secret",
|
|
683
|
-
"clientSecret"
|
|
704
|
+
"clientSecret",
|
|
705
|
+
"key",
|
|
706
|
+
"apiKey"
|
|
684
707
|
]);
|
|
685
708
|
logRequest(method, url, headers, body) {
|
|
686
709
|
if (!this.verbose) return;
|
|
@@ -963,6 +986,13 @@ function withErrorHandler(fn) {
|
|
|
963
986
|
}
|
|
964
987
|
if (err instanceof GdbClientError && err.status === 401) {
|
|
965
988
|
printError("Authentication failed. Please run `geonic login` to re-authenticate.");
|
|
989
|
+
} else if (err instanceof GdbClientError && err.status === 403) {
|
|
990
|
+
const detail = (err.ngsiError?.detail ?? err.ngsiError?.description ?? "").toLowerCase();
|
|
991
|
+
if (detail.includes("entity type") || detail.includes("allowedentitytypes")) {
|
|
992
|
+
printError(`Entity type restriction: ${err.message}`);
|
|
993
|
+
} else {
|
|
994
|
+
printError(err.message);
|
|
995
|
+
}
|
|
966
996
|
} else if (err instanceof Error) {
|
|
967
997
|
printError(err.message);
|
|
968
998
|
} else {
|
|
@@ -1323,6 +1353,126 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1323
1353
|
]);
|
|
1324
1354
|
}
|
|
1325
1355
|
|
|
1356
|
+
// src/commands/me-api-keys.ts
|
|
1357
|
+
function addMeApiKeysSubcommand(me) {
|
|
1358
|
+
const apiKeys = me.command("api-keys").description("Manage your API keys");
|
|
1359
|
+
const list = apiKeys.command("list").description("List your API keys").action(
|
|
1360
|
+
withErrorHandler(async (_opts, cmd) => {
|
|
1361
|
+
const client = createClient(cmd);
|
|
1362
|
+
const format = getFormat(cmd);
|
|
1363
|
+
const response = await client.rawRequest("GET", "/me/api-keys");
|
|
1364
|
+
outputResponse(response, format);
|
|
1365
|
+
})
|
|
1366
|
+
);
|
|
1367
|
+
addExamples(list, [
|
|
1368
|
+
{
|
|
1369
|
+
description: "List your API keys",
|
|
1370
|
+
command: "geonic me api-keys list"
|
|
1371
|
+
}
|
|
1372
|
+
]);
|
|
1373
|
+
const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Allowed scopes (comma-separated)").option("--origins <origins>", "Allowed origins (comma-separated)").option("--entity-types <types>", "Allowed entity types (comma-separated)").option("--rate-limit <n>", "Rate limit per minute").option("--save", "Save the API key to config for automatic use").action(
|
|
1374
|
+
withErrorHandler(async (json, _opts, cmd) => {
|
|
1375
|
+
const opts = cmd.opts();
|
|
1376
|
+
if (opts.origins !== void 0) {
|
|
1377
|
+
const parsed = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1378
|
+
if (parsed.length === 0) {
|
|
1379
|
+
printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
|
|
1380
|
+
process.exit(1);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
let body;
|
|
1384
|
+
if (json) {
|
|
1385
|
+
body = await parseJsonInput(json);
|
|
1386
|
+
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit) {
|
|
1387
|
+
const payload = {};
|
|
1388
|
+
if (opts.name) payload.name = opts.name;
|
|
1389
|
+
if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1390
|
+
if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1391
|
+
if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1392
|
+
if (opts.rateLimit) {
|
|
1393
|
+
const raw = opts.rateLimit.trim();
|
|
1394
|
+
if (!/^\d+$/.test(raw)) {
|
|
1395
|
+
printError("--rate-limit must be a positive integer.");
|
|
1396
|
+
process.exit(1);
|
|
1397
|
+
}
|
|
1398
|
+
const perMinute = Number(raw);
|
|
1399
|
+
if (perMinute <= 0) {
|
|
1400
|
+
printError("--rate-limit must be a positive integer.");
|
|
1401
|
+
process.exit(1);
|
|
1402
|
+
}
|
|
1403
|
+
payload.rateLimit = { perMinute };
|
|
1404
|
+
}
|
|
1405
|
+
body = payload;
|
|
1406
|
+
} else {
|
|
1407
|
+
body = await parseJsonInput();
|
|
1408
|
+
}
|
|
1409
|
+
if (body && typeof body === "object" && "allowedOrigins" in body) {
|
|
1410
|
+
const origins = body.allowedOrigins;
|
|
1411
|
+
if (Array.isArray(origins) && origins.filter((o) => typeof o === "string" && o.trim() !== "").length === 0) {
|
|
1412
|
+
printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
|
|
1413
|
+
process.exit(1);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
const client = createClient(cmd);
|
|
1417
|
+
const format = getFormat(cmd);
|
|
1418
|
+
const response = await client.rawRequest("POST", "/me/api-keys", { body });
|
|
1419
|
+
const data = response.data;
|
|
1420
|
+
if (opts.save) {
|
|
1421
|
+
const globalOpts = resolveOptions(cmd);
|
|
1422
|
+
const key = data.key;
|
|
1423
|
+
if (!key) {
|
|
1424
|
+
printError("Response missing key. API key was created, but it could not be saved.");
|
|
1425
|
+
outputResponse(response, format);
|
|
1426
|
+
process.exitCode = 1;
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const config = loadConfig(globalOpts.profile);
|
|
1430
|
+
config.apiKey = key;
|
|
1431
|
+
saveConfig(config, globalOpts.profile);
|
|
1432
|
+
console.error("API key saved to config. X-Api-Key header will be sent automatically.");
|
|
1433
|
+
} else {
|
|
1434
|
+
printWarning("Save the API key now \u2014 it will not be shown again. Use --save to store it automatically.");
|
|
1435
|
+
}
|
|
1436
|
+
outputResponse(response, format);
|
|
1437
|
+
console.error("API key created.");
|
|
1438
|
+
})
|
|
1439
|
+
);
|
|
1440
|
+
addExamples(create, [
|
|
1441
|
+
{
|
|
1442
|
+
description: "Create an API key with flags",
|
|
1443
|
+
command: "geonic me api-keys create --name my-app --scopes read:entities --origins 'https://example.com'"
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
description: "Create and save API key to config",
|
|
1447
|
+
command: "geonic me api-keys create --name my-app --save"
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
description: "Create an API key from JSON",
|
|
1451
|
+
command: `geonic me api-keys create '{"name":"my-app","allowedScopes":["read:entities"]}'`
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
description: "Create an API key with rate limiting",
|
|
1455
|
+
command: "geonic me api-keys create --name my-app --rate-limit 100"
|
|
1456
|
+
}
|
|
1457
|
+
]);
|
|
1458
|
+
const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
|
|
1459
|
+
withErrorHandler(async (keyId, _opts, cmd) => {
|
|
1460
|
+
const client = createClient(cmd);
|
|
1461
|
+
await client.rawRequest(
|
|
1462
|
+
"DELETE",
|
|
1463
|
+
`/me/api-keys/${encodeURIComponent(String(keyId))}`
|
|
1464
|
+
);
|
|
1465
|
+
console.error("API key deleted.");
|
|
1466
|
+
})
|
|
1467
|
+
);
|
|
1468
|
+
addExamples(del, [
|
|
1469
|
+
{
|
|
1470
|
+
description: "Delete an API key",
|
|
1471
|
+
command: "geonic me api-keys delete <key-id>"
|
|
1472
|
+
}
|
|
1473
|
+
]);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1326
1476
|
// src/commands/auth.ts
|
|
1327
1477
|
function createLoginCommand() {
|
|
1328
1478
|
return new Command("login").description("Authenticate and save token").option("--client-credentials", "Use OAuth 2.0 Client Credentials flow").option("--client-id <id>", "OAuth client ID").option("--client-secret <secret>", "OAuth client secret").option("--scope <scopes>", "OAuth scopes (space-separated)").option("--tenant-id <id>", "Tenant ID for scoped authentication").action(
|
|
@@ -1450,6 +1600,117 @@ function createMeAction() {
|
|
|
1450
1600
|
printInfo(`Profile: ${profileName}`);
|
|
1451
1601
|
});
|
|
1452
1602
|
}
|
|
1603
|
+
async function fetchNonce(baseUrl, apiKey) {
|
|
1604
|
+
const origin = new URL(baseUrl).origin;
|
|
1605
|
+
const url = new URL("/auth/nonce", baseUrl).toString();
|
|
1606
|
+
const response = await fetch(url, {
|
|
1607
|
+
method: "POST",
|
|
1608
|
+
headers: {
|
|
1609
|
+
"Content-Type": "application/json",
|
|
1610
|
+
"Origin": origin
|
|
1611
|
+
},
|
|
1612
|
+
body: JSON.stringify({ api_key: apiKey })
|
|
1613
|
+
});
|
|
1614
|
+
if (!response.ok) {
|
|
1615
|
+
const text = await response.text();
|
|
1616
|
+
throw new Error(`Nonce request failed: ${text || `HTTP ${response.status}`}`);
|
|
1617
|
+
}
|
|
1618
|
+
return await response.json();
|
|
1619
|
+
}
|
|
1620
|
+
function createNonceCommand() {
|
|
1621
|
+
return new Command("nonce").description("Get a nonce and PoW challenge for API key authentication").option("--api-key <key>", "API key to get nonce for").action(
|
|
1622
|
+
withErrorHandler(async (...args) => {
|
|
1623
|
+
const cmd = args[args.length - 1];
|
|
1624
|
+
const nonceOpts = cmd.opts();
|
|
1625
|
+
const globalOpts = resolveOptions(cmd);
|
|
1626
|
+
const apiKey = nonceOpts.apiKey ?? globalOpts.apiKey;
|
|
1627
|
+
if (!apiKey) {
|
|
1628
|
+
printError("API key is required. Use --api-key or configure it with `geonic config set api-key <key>`.");
|
|
1629
|
+
process.exit(1);
|
|
1630
|
+
}
|
|
1631
|
+
if (!globalOpts.url) {
|
|
1632
|
+
printError("No URL configured. Use `geonic config set url <url>` or pass --url.");
|
|
1633
|
+
process.exit(1);
|
|
1634
|
+
}
|
|
1635
|
+
const baseUrl = validateUrl(globalOpts.url);
|
|
1636
|
+
const data = await fetchNonce(baseUrl, apiKey);
|
|
1637
|
+
const format = getFormat(cmd);
|
|
1638
|
+
outputResponse({ status: 200, headers: new Headers(), data }, format);
|
|
1639
|
+
})
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
function hasLeadingZeroBits(hash, bits) {
|
|
1643
|
+
const fullBytes = Math.floor(bits / 8);
|
|
1644
|
+
const remainingBits = bits % 8;
|
|
1645
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
1646
|
+
if (hash[i] !== 0) return false;
|
|
1647
|
+
}
|
|
1648
|
+
if (remainingBits > 0) {
|
|
1649
|
+
const mask = 255 << 8 - remainingBits;
|
|
1650
|
+
if ((hash[fullBytes] & mask) !== 0) return false;
|
|
1651
|
+
}
|
|
1652
|
+
return true;
|
|
1653
|
+
}
|
|
1654
|
+
var MAX_POW_ITERATIONS = 1e7;
|
|
1655
|
+
function solvePoW(challenge, difficulty) {
|
|
1656
|
+
for (let nonce = 0; nonce < MAX_POW_ITERATIONS; nonce++) {
|
|
1657
|
+
const hash = createHash("sha256").update(`${challenge}${nonce}`).digest();
|
|
1658
|
+
if (hasLeadingZeroBits(hash, difficulty)) return nonce;
|
|
1659
|
+
}
|
|
1660
|
+
throw new Error(`PoW could not be solved within ${MAX_POW_ITERATIONS} iterations`);
|
|
1661
|
+
}
|
|
1662
|
+
function createTokenExchangeCommand() {
|
|
1663
|
+
return new Command("token-exchange").description("Exchange API key for a session JWT via nonce + PoW").option("--api-key <key>", "API key to exchange").option("--save", "Save the obtained token to profile config").action(
|
|
1664
|
+
withErrorHandler(async (...args) => {
|
|
1665
|
+
const cmd = args[args.length - 1];
|
|
1666
|
+
const exchangeOpts = cmd.opts();
|
|
1667
|
+
const globalOpts = resolveOptions(cmd);
|
|
1668
|
+
const apiKey = exchangeOpts.apiKey ?? globalOpts.apiKey;
|
|
1669
|
+
if (!apiKey) {
|
|
1670
|
+
printError("API key is required. Use --api-key or configure it with `geonic config set api-key <key>`.");
|
|
1671
|
+
process.exit(1);
|
|
1672
|
+
}
|
|
1673
|
+
if (!globalOpts.url) {
|
|
1674
|
+
printError("No URL configured. Use `geonic config set url <url>` or pass --url.");
|
|
1675
|
+
process.exit(1);
|
|
1676
|
+
}
|
|
1677
|
+
const baseUrl = validateUrl(globalOpts.url);
|
|
1678
|
+
const origin = new URL(baseUrl).origin;
|
|
1679
|
+
const nonceData = await fetchNonce(baseUrl, apiKey);
|
|
1680
|
+
printInfo(`Nonce received. Solving PoW (difficulty=${nonceData.difficulty})...`);
|
|
1681
|
+
const powNonce = solvePoW(nonceData.challenge, nonceData.difficulty);
|
|
1682
|
+
const tokenUrl = new URL("/oauth/token", baseUrl).toString();
|
|
1683
|
+
const tokenResponse = await fetch(tokenUrl, {
|
|
1684
|
+
method: "POST",
|
|
1685
|
+
headers: {
|
|
1686
|
+
"Content-Type": "application/json",
|
|
1687
|
+
"Origin": origin
|
|
1688
|
+
},
|
|
1689
|
+
body: JSON.stringify({
|
|
1690
|
+
grant_type: "api_key",
|
|
1691
|
+
api_key: apiKey,
|
|
1692
|
+
nonce: nonceData.nonce,
|
|
1693
|
+
proof: String(powNonce)
|
|
1694
|
+
})
|
|
1695
|
+
});
|
|
1696
|
+
if (!tokenResponse.ok) {
|
|
1697
|
+
const text = await tokenResponse.text();
|
|
1698
|
+
throw new Error(`Token exchange failed: ${text || `HTTP ${tokenResponse.status}`}`);
|
|
1699
|
+
}
|
|
1700
|
+
const tokenData = await tokenResponse.json();
|
|
1701
|
+
if (exchangeOpts.save) {
|
|
1702
|
+
const config = loadConfig(globalOpts.profile);
|
|
1703
|
+
config.token = tokenData.access_token;
|
|
1704
|
+
saveConfig(config, globalOpts.profile);
|
|
1705
|
+
printSuccess("Token exchange successful. Token saved to config.");
|
|
1706
|
+
} else {
|
|
1707
|
+
const format = getFormat(cmd);
|
|
1708
|
+
outputResponse({ status: tokenResponse.status, headers: tokenResponse.headers, data: tokenData }, format);
|
|
1709
|
+
printSuccess("Token exchange successful.");
|
|
1710
|
+
}
|
|
1711
|
+
})
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1453
1714
|
function registerAuthCommands(program2) {
|
|
1454
1715
|
const auth = program2.command("auth").description("Manage authentication");
|
|
1455
1716
|
const login = createLoginCommand();
|
|
@@ -1480,6 +1741,22 @@ function registerAuthCommands(program2) {
|
|
|
1480
1741
|
}
|
|
1481
1742
|
]);
|
|
1482
1743
|
auth.addCommand(logout);
|
|
1744
|
+
const nonce = createNonceCommand();
|
|
1745
|
+
addExamples(nonce, [
|
|
1746
|
+
{
|
|
1747
|
+
description: "Get a nonce for API key authentication",
|
|
1748
|
+
command: "geonic auth nonce --api-key gdb_abcdef..."
|
|
1749
|
+
}
|
|
1750
|
+
]);
|
|
1751
|
+
auth.addCommand(nonce);
|
|
1752
|
+
const tokenExchange = createTokenExchangeCommand();
|
|
1753
|
+
addExamples(tokenExchange, [
|
|
1754
|
+
{
|
|
1755
|
+
description: "Exchange API key for a JWT and save it",
|
|
1756
|
+
command: "geonic auth token-exchange --api-key gdb_abcdef... --save"
|
|
1757
|
+
}
|
|
1758
|
+
]);
|
|
1759
|
+
auth.addCommand(tokenExchange);
|
|
1483
1760
|
const me = program2.command("me").description("Display current authenticated user and manage user resources");
|
|
1484
1761
|
const meInfo = me.command("info", { isDefault: true, hidden: true }).description("Display current authenticated user").action(createMeAction());
|
|
1485
1762
|
addExamples(me, [
|
|
@@ -1490,6 +1767,10 @@ function registerAuthCommands(program2) {
|
|
|
1490
1767
|
{
|
|
1491
1768
|
description: "List your OAuth clients",
|
|
1492
1769
|
command: "geonic me oauth-clients list"
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
description: "List your API keys",
|
|
1773
|
+
command: "geonic me api-keys list"
|
|
1493
1774
|
}
|
|
1494
1775
|
]);
|
|
1495
1776
|
addExamples(meInfo, [
|
|
@@ -1499,6 +1780,7 @@ function registerAuthCommands(program2) {
|
|
|
1499
1780
|
}
|
|
1500
1781
|
]);
|
|
1501
1782
|
addMeOAuthClientsSubcommand(me);
|
|
1783
|
+
addMeApiKeysSubcommand(me);
|
|
1502
1784
|
program2.addCommand(createLoginCommand(), { hidden: true });
|
|
1503
1785
|
program2.addCommand(createLogoutCommand(), { hidden: true });
|
|
1504
1786
|
const hiddenWhoami = new Command("whoami").description("Display current authenticated user").action(createMeAction());
|
|
@@ -1696,7 +1978,7 @@ function registerAttrsSubcommand(entitiesCmd) {
|
|
|
1696
1978
|
// src/commands/entities.ts
|
|
1697
1979
|
function registerEntitiesCommand(program2) {
|
|
1698
1980
|
const entities = program2.command("entities").description("Manage context entities");
|
|
1699
|
-
const list = entities.command("list").description("List entities with optional filters").option("--type <type>", "Filter by entity type").option("--id-pattern <pat>", "Filter by entity ID pattern (regex)").option("--query <q>", "NGSI query expression").option("--attrs <a,b>", "Comma-separated list of attributes to include").option("--georel <rel>", "Geo-relationship (e.g. near;maxDistance:1000)").option("--geometry <geo>", "Geometry type for geo-query (e.g. point)").option("--coords <coords>", "Coordinates for geo-query").option("--spatial-id <zfxy>", "Spatial ID filter (ZFXY tile)").option("--limit <n>", "Maximum number of entities to return", parseInt).option("--offset <n>", "Skip first N entities", parseInt).option("--order-by <field>", "Order results by field").option("--count", "Include total count in response").option("--key-values", "Request simplified key-value format").action(
|
|
1981
|
+
const list = entities.command("list").description("List entities with optional filters").option("--type <type>", "Filter by entity type").option("--id-pattern <pat>", "Filter by entity ID pattern (regex)").option("--query <q>", "NGSI query expression").option("--attrs <a,b>", "Comma-separated list of attributes to include").option("--georel <rel>", "Geo-relationship (e.g. near;maxDistance:1000)").option("--geometry <geo>", "Geometry type for geo-query (e.g. point)").option("--coords <coords>", "Coordinates for geo-query").option("--spatial-id <zfxy>", "Spatial ID filter (ZFXY tile)").option("--limit <n>", "Maximum number of entities to return", parseInt).option("--offset <n>", "Skip first N entities", parseInt).option("--order-by <field>", "Order results by field").option("--count", "Include total count in response").option("--count-only", "Only show the total count without listing entities").option("--key-values", "Request simplified key-value format").action(
|
|
1700
1982
|
withErrorHandler(async (opts, cmd) => {
|
|
1701
1983
|
const client = createClient(cmd);
|
|
1702
1984
|
const format = getFormat(cmd);
|
|
@@ -1712,10 +1994,15 @@ function registerEntitiesCommand(program2) {
|
|
|
1712
1994
|
if (opts.limit !== void 0) params.limit = String(opts.limit);
|
|
1713
1995
|
if (opts.offset !== void 0) params.offset = String(opts.offset);
|
|
1714
1996
|
if (opts.orderBy) params.orderBy = String(opts.orderBy);
|
|
1715
|
-
if (opts.count) params.count = "true";
|
|
1997
|
+
if (opts.count || opts.countOnly) params.count = "true";
|
|
1998
|
+
if (opts.countOnly) params.limit = "0";
|
|
1716
1999
|
if (opts.keyValues) params.options = "keyValues";
|
|
1717
2000
|
const response = await client.get("/entities", params);
|
|
1718
|
-
|
|
2001
|
+
if (opts.countOnly) {
|
|
2002
|
+
printCount(response.count ?? 0);
|
|
2003
|
+
} else {
|
|
2004
|
+
outputResponse(response, format, !!opts.count);
|
|
2005
|
+
}
|
|
1719
2006
|
})
|
|
1720
2007
|
);
|
|
1721
2008
|
addExamples(list, [
|
|
@@ -1774,6 +2061,10 @@ function registerEntitiesCommand(program2) {
|
|
|
1774
2061
|
{
|
|
1775
2062
|
description: "Get total count with results",
|
|
1776
2063
|
command: "geonic entities list --type Sensor --count"
|
|
2064
|
+
},
|
|
2065
|
+
{
|
|
2066
|
+
description: "Get only the total count (no entity data)",
|
|
2067
|
+
command: "geonic entities list --type Sensor --count-only"
|
|
1777
2068
|
}
|
|
1778
2069
|
]);
|
|
1779
2070
|
const get = entities.command("get").description("Get a single entity by ID").argument("<id>", "Entity ID").option("--key-values", "Request simplified key-value format").action(
|
|
@@ -3036,6 +3327,190 @@ function registerCaddeCommand(parent) {
|
|
|
3036
3327
|
]);
|
|
3037
3328
|
}
|
|
3038
3329
|
|
|
3330
|
+
// src/commands/admin/api-keys.ts
|
|
3331
|
+
function validateOrigins(body, opts) {
|
|
3332
|
+
if (opts.origins !== void 0) {
|
|
3333
|
+
const origins = String(opts.origins).split(",").map((s) => s.trim()).filter(Boolean);
|
|
3334
|
+
if (origins.length === 0) {
|
|
3335
|
+
printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
|
|
3336
|
+
process.exit(1);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
if (body && typeof body === "object" && "allowedOrigins" in body) {
|
|
3340
|
+
const origins = body.allowedOrigins;
|
|
3341
|
+
if (Array.isArray(origins) && origins.filter((o) => typeof o === "string" && o.trim() !== "").length === 0) {
|
|
3342
|
+
printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
|
|
3343
|
+
process.exit(1);
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
function buildBodyFromFlags(opts) {
|
|
3348
|
+
const payload = {};
|
|
3349
|
+
if (opts.name) payload.name = opts.name;
|
|
3350
|
+
if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3351
|
+
if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3352
|
+
if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3353
|
+
if (opts.rateLimit) {
|
|
3354
|
+
const raw = String(opts.rateLimit).trim();
|
|
3355
|
+
if (!/^\d+$/.test(raw)) {
|
|
3356
|
+
printError("--rate-limit must be a positive integer.");
|
|
3357
|
+
process.exit(1);
|
|
3358
|
+
}
|
|
3359
|
+
const perMinute = Number(raw);
|
|
3360
|
+
if (perMinute <= 0) {
|
|
3361
|
+
printError("--rate-limit must be a positive integer.");
|
|
3362
|
+
process.exit(1);
|
|
3363
|
+
}
|
|
3364
|
+
payload.rateLimit = { perMinute };
|
|
3365
|
+
}
|
|
3366
|
+
if (opts.tenantId) payload.tenantId = opts.tenantId;
|
|
3367
|
+
return payload;
|
|
3368
|
+
}
|
|
3369
|
+
function registerApiKeysCommand(parent) {
|
|
3370
|
+
const apiKeys = parent.command("api-keys").description("Manage API keys");
|
|
3371
|
+
const list = apiKeys.command("list").description("List all API keys").option("--tenant-id <id>", "Filter by tenant ID").action(
|
|
3372
|
+
withErrorHandler(async (_opts, cmd) => {
|
|
3373
|
+
const opts = cmd.opts();
|
|
3374
|
+
const client = createClient(cmd);
|
|
3375
|
+
const format = getFormat(cmd);
|
|
3376
|
+
const params = {};
|
|
3377
|
+
if (opts.tenantId) params.tenantId = opts.tenantId;
|
|
3378
|
+
const response = await client.rawRequest("GET", "/admin/api-keys", {
|
|
3379
|
+
params
|
|
3380
|
+
});
|
|
3381
|
+
outputResponse(response, format);
|
|
3382
|
+
})
|
|
3383
|
+
);
|
|
3384
|
+
addExamples(list, [
|
|
3385
|
+
{
|
|
3386
|
+
description: "List all API keys",
|
|
3387
|
+
command: "geonic admin api-keys list"
|
|
3388
|
+
},
|
|
3389
|
+
{
|
|
3390
|
+
description: "List API keys for a specific tenant",
|
|
3391
|
+
command: "geonic admin api-keys list --tenant-id <tenant-id>"
|
|
3392
|
+
}
|
|
3393
|
+
]);
|
|
3394
|
+
const get = apiKeys.command("get <keyId>").description("Get an API key by ID").action(
|
|
3395
|
+
withErrorHandler(async (keyId, _opts, cmd) => {
|
|
3396
|
+
const client = createClient(cmd);
|
|
3397
|
+
const format = getFormat(cmd);
|
|
3398
|
+
const response = await client.rawRequest(
|
|
3399
|
+
"GET",
|
|
3400
|
+
`/admin/api-keys/${encodeURIComponent(String(keyId))}`
|
|
3401
|
+
);
|
|
3402
|
+
outputResponse(response, format);
|
|
3403
|
+
})
|
|
3404
|
+
);
|
|
3405
|
+
addExamples(get, [
|
|
3406
|
+
{
|
|
3407
|
+
description: "Get an API key by ID",
|
|
3408
|
+
command: "geonic admin api-keys get <key-id>"
|
|
3409
|
+
}
|
|
3410
|
+
]);
|
|
3411
|
+
const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Comma-separated scopes").option("--origins <origins>", "Comma-separated origins").option("--entity-types <types>", "Comma-separated entity types").option("--rate-limit <n>", "Rate limit per minute").option("--tenant-id <id>", "Tenant ID").option("--save", "Save the API key to profile config").action(
|
|
3412
|
+
withErrorHandler(async (json, _opts, cmd) => {
|
|
3413
|
+
const opts = cmd.opts();
|
|
3414
|
+
validateOrigins(void 0, opts);
|
|
3415
|
+
let body;
|
|
3416
|
+
if (json) {
|
|
3417
|
+
body = await parseJsonInput(json);
|
|
3418
|
+
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.tenantId) {
|
|
3419
|
+
body = buildBodyFromFlags(opts);
|
|
3420
|
+
} else {
|
|
3421
|
+
body = await parseJsonInput();
|
|
3422
|
+
}
|
|
3423
|
+
validateOrigins(body, {});
|
|
3424
|
+
const client = createClient(cmd);
|
|
3425
|
+
const format = getFormat(cmd);
|
|
3426
|
+
const response = await client.rawRequest("POST", "/admin/api-keys", {
|
|
3427
|
+
body
|
|
3428
|
+
});
|
|
3429
|
+
const data = response.data;
|
|
3430
|
+
if (opts.save) {
|
|
3431
|
+
const globalOpts = resolveOptions(cmd);
|
|
3432
|
+
const key = data.key;
|
|
3433
|
+
if (!key) {
|
|
3434
|
+
printError("Response missing key. API key was created, but it could not be saved.");
|
|
3435
|
+
outputResponse(response, format);
|
|
3436
|
+
process.exitCode = 1;
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
const config = loadConfig(globalOpts.profile);
|
|
3440
|
+
config.apiKey = key;
|
|
3441
|
+
saveConfig(config, globalOpts.profile);
|
|
3442
|
+
console.error("API key saved to config. X-Api-Key header will be sent automatically.");
|
|
3443
|
+
} else {
|
|
3444
|
+
printWarning("Save the API key now \u2014 it will not be shown again. Use --save to store it automatically.");
|
|
3445
|
+
}
|
|
3446
|
+
outputResponse(response, format);
|
|
3447
|
+
console.error("API key created.");
|
|
3448
|
+
})
|
|
3449
|
+
);
|
|
3450
|
+
addExamples(create, [
|
|
3451
|
+
{
|
|
3452
|
+
description: "Create an API key with flags",
|
|
3453
|
+
command: "geonic admin api-keys create --name my-key --scopes entities:read,entities:write --origins '*'"
|
|
3454
|
+
},
|
|
3455
|
+
{
|
|
3456
|
+
description: "Create an API key from JSON and save to config",
|
|
3457
|
+
command: "geonic admin api-keys create @key.json --save"
|
|
3458
|
+
}
|
|
3459
|
+
]);
|
|
3460
|
+
const update = apiKeys.command("update <keyId> [json]").description("Update an API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Comma-separated scopes").option("--origins <origins>", "Comma-separated origins").option("--entity-types <types>", "Comma-separated entity types").option("--rate-limit <n>", "Rate limit per minute").action(
|
|
3461
|
+
withErrorHandler(
|
|
3462
|
+
async (keyId, json, _opts, cmd) => {
|
|
3463
|
+
const opts = cmd.opts();
|
|
3464
|
+
validateOrigins(void 0, opts);
|
|
3465
|
+
let body;
|
|
3466
|
+
if (json) {
|
|
3467
|
+
body = await parseJsonInput(json);
|
|
3468
|
+
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit) {
|
|
3469
|
+
body = buildBodyFromFlags(opts);
|
|
3470
|
+
} else {
|
|
3471
|
+
body = await parseJsonInput();
|
|
3472
|
+
}
|
|
3473
|
+
validateOrigins(body, {});
|
|
3474
|
+
const client = createClient(cmd);
|
|
3475
|
+
const format = getFormat(cmd);
|
|
3476
|
+
const response = await client.rawRequest(
|
|
3477
|
+
"PATCH",
|
|
3478
|
+
`/admin/api-keys/${encodeURIComponent(String(keyId))}`,
|
|
3479
|
+
{ body }
|
|
3480
|
+
);
|
|
3481
|
+
outputResponse(response, format);
|
|
3482
|
+
console.error("API key updated.");
|
|
3483
|
+
}
|
|
3484
|
+
)
|
|
3485
|
+
);
|
|
3486
|
+
addExamples(update, [
|
|
3487
|
+
{
|
|
3488
|
+
description: "Update an API key name",
|
|
3489
|
+
command: "geonic admin api-keys update <key-id> --name new-name"
|
|
3490
|
+
},
|
|
3491
|
+
{
|
|
3492
|
+
description: "Update an API key from a JSON file",
|
|
3493
|
+
command: "geonic admin api-keys update <key-id> @key.json"
|
|
3494
|
+
}
|
|
3495
|
+
]);
|
|
3496
|
+
const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
|
|
3497
|
+
withErrorHandler(async (keyId, _opts, cmd) => {
|
|
3498
|
+
const client = createClient(cmd);
|
|
3499
|
+
await client.rawRequest(
|
|
3500
|
+
"DELETE",
|
|
3501
|
+
`/admin/api-keys/${encodeURIComponent(String(keyId))}`
|
|
3502
|
+
);
|
|
3503
|
+
console.error("API key deleted.");
|
|
3504
|
+
})
|
|
3505
|
+
);
|
|
3506
|
+
addExamples(del, [
|
|
3507
|
+
{
|
|
3508
|
+
description: "Delete an API key",
|
|
3509
|
+
command: "geonic admin api-keys delete <key-id>"
|
|
3510
|
+
}
|
|
3511
|
+
]);
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3039
3514
|
// src/commands/admin/index.ts
|
|
3040
3515
|
function registerAdminCommand(program2) {
|
|
3041
3516
|
const admin = program2.command("admin").description("Manage admin resources");
|
|
@@ -3043,6 +3518,7 @@ function registerAdminCommand(program2) {
|
|
|
3043
3518
|
registerUsersCommand(admin);
|
|
3044
3519
|
registerPoliciesCommand(admin);
|
|
3045
3520
|
registerOAuthClientsCommand(admin);
|
|
3521
|
+
registerApiKeysCommand(admin);
|
|
3046
3522
|
registerCaddeCommand(admin);
|
|
3047
3523
|
}
|
|
3048
3524
|
|