@geolonia/geonicdb-cli 0.7.0 → 0.9.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/dist/index.js CHANGED
@@ -186,6 +186,15 @@ function printInfo(message) {
186
186
  function printWarning(message) {
187
187
  console.error(chalk.yellow(message));
188
188
  }
189
+ function printApiKeyBox(key) {
190
+ const border = "\u2500".repeat(key.length + 4);
191
+ console.error("");
192
+ console.error(chalk.green(` \u250C${border}\u2510`));
193
+ console.error(chalk.green(` \u2502 ${chalk.bold(key)} \u2502`));
194
+ console.error(chalk.green(` \u2514${border}\u2518`));
195
+ console.error("");
196
+ console.error(chalk.yellow("\u26A0 \u3053\u306E API \u30AD\u30FC\u5024\u3092\u5B89\u5168\u306B\u4FDD\u5B58\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4E8C\u5EA6\u3068\u8868\u793A\u3055\u308C\u307E\u305B\u3093\u3002"));
197
+ }
189
198
  function printCount(count) {
190
199
  console.log(chalk.dim(`Count: ${count}`));
191
200
  }
@@ -785,6 +794,15 @@ var GdbClient = class _GdbClient {
785
794
  canRefresh() {
786
795
  return (!!this.refreshToken || !!this.clientId && !!this.clientSecret) && !this.apiKey;
787
796
  }
797
+ /** Check whether an error indicates an authentication/token problem that may be resolved by refreshing. */
798
+ static isTokenError(err) {
799
+ if (err.status === 401) return true;
800
+ if (err.status === 403) {
801
+ const msg = (err.message ?? "").toLowerCase();
802
+ return msg.includes("not assigned to any tenant") || msg.includes("invalid token");
803
+ }
804
+ return false;
805
+ }
788
806
  async performTokenRefresh() {
789
807
  if (this.refreshPromise) return this.refreshPromise;
790
808
  this.refreshPromise = this.doRefresh();
@@ -888,7 +906,7 @@ var GdbClient = class _GdbClient {
888
906
  try {
889
907
  return await this.executeRequest(method, path, options);
890
908
  } catch (err) {
891
- if (err instanceof GdbClientError && err.status === 401 && this.canRefresh()) {
909
+ if (err instanceof GdbClientError && _GdbClient.isTokenError(err) && this.canRefresh()) {
892
910
  const refreshed = await this.performTokenRefresh();
893
911
  if (refreshed) {
894
912
  return await this.executeRequest(method, path, options);
@@ -917,7 +935,7 @@ var GdbClient = class _GdbClient {
917
935
  try {
918
936
  return await this.executeRawRequest(method, path, options);
919
937
  } catch (err) {
920
- if (err instanceof GdbClientError && err.status === 401 && this.canRefresh()) {
938
+ if (err instanceof GdbClientError && _GdbClient.isTokenError(err) && this.canRefresh()) {
921
939
  const refreshed = await this.performTokenRefresh();
922
940
  if (refreshed) {
923
941
  return await this.executeRawRequest(method, path, options);
@@ -937,31 +955,6 @@ var GdbClientError = class extends Error {
937
955
  };
938
956
 
939
957
  // src/helpers.ts
940
- var SCOPES_HELP_NOTES = [
941
- "Valid scopes:",
942
- " read:entities, write:entities, read:subscriptions, write:subscriptions,",
943
- " read:registrations, write:registrations, read:rules, write:rules,",
944
- " read:custom-data-models, write:custom-data-models,",
945
- " admin:users, admin:tenants, admin:policies, admin:oauth-clients,",
946
- " admin:api-keys, admin:metrics",
947
- "",
948
- "admin:X implies both read:X and write:X.",
949
- "write:X does NOT imply read:X \u2014 specify both if needed."
950
- ];
951
- var API_KEY_SCOPES_HELP_NOTES = [
952
- "Valid scopes:",
953
- " read:entities, write:entities, read:subscriptions, write:subscriptions,",
954
- " read:registrations, write:registrations"
955
- ];
956
- var VALID_PERMISSIONS = /* @__PURE__ */ new Set(["read", "write", "create", "update", "delete"]);
957
- function parsePermissions(raw) {
958
- const permissions = raw.split(",").map((s) => s.trim()).filter(Boolean);
959
- if (permissions.length === 0 || permissions.some((p) => !VALID_PERMISSIONS.has(p))) {
960
- printError("--permissions must be a comma-separated list of: read, write, create, update, delete");
961
- process.exit(1);
962
- }
963
- return permissions;
964
- }
965
958
  function resolveOptions(cmd) {
966
959
  const opts = cmd.optsWithGlobals();
967
960
  const config = loadConfig(opts.profile);
@@ -1027,6 +1020,8 @@ function withErrorHandler(fn) {
1027
1020
  }
1028
1021
  if (err instanceof GdbClientError && err.status === 401) {
1029
1022
  printError("Authentication failed. Please re-authenticate (e.g., `geonic auth login` or check your API key).");
1023
+ } else if (err instanceof GdbClientError && err.status === 403 && /not assigned to any tenant|invalid token/i.test(err.message)) {
1024
+ printError("Authentication failed. Please re-authenticate (e.g., `geonic auth login` or check your API key).");
1030
1025
  } else if (err instanceof GdbClientError && err.status === 403) {
1031
1026
  const detail = (err.ngsiError?.detail ?? err.ngsiError?.description ?? "").toLowerCase();
1032
1027
  if (detail.includes("entity type") || detail.includes("allowedentitytypes")) {
@@ -1335,16 +1330,16 @@ function addMeOAuthClientsSubcommand(me) {
1335
1330
  command: "geonic me oauth-clients list"
1336
1331
  }
1337
1332
  ]);
1338
- const create = oauthClients.command("create [json]").description("Create a new OAuth client").option("--name <name>", "Client name").option("--scopes <scopes>", "Allowed scopes (comma-separated)").option("--save", "Save credentials to config for automatic re-authentication").action(
1333
+ const create = oauthClients.command("create [json]").description("Create a new OAuth client").option("--name <name>", "Client name").option("--policy <policyId>", "Policy ID to attach").option("--save", "Save credentials to config for automatic re-authentication").action(
1339
1334
  withErrorHandler(async (json, _opts, cmd) => {
1340
1335
  const opts = cmd.opts();
1341
1336
  let body;
1342
1337
  if (json) {
1343
1338
  body = await parseJsonInput(json);
1344
- } else if (opts.name || opts.scopes) {
1339
+ } else if (opts.name || opts.policy) {
1345
1340
  const payload = {};
1346
- if (opts.name) payload.clientName = opts.name;
1347
- if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim());
1341
+ if (opts.name) payload.name = opts.name;
1342
+ if (opts.policy) payload.policyId = opts.policy;
1348
1343
  body = payload;
1349
1344
  } else {
1350
1345
  body = await parseJsonInput();
@@ -1367,8 +1362,7 @@ function addMeOAuthClientsSubcommand(me) {
1367
1362
  const tokenResult = await clientCredentialsGrant({
1368
1363
  baseUrl,
1369
1364
  clientId,
1370
- clientSecret,
1371
- scope: data.allowedScopes?.join(" ")
1365
+ clientSecret
1372
1366
  });
1373
1367
  const config = loadConfig(globalOpts.profile);
1374
1368
  config.clientId = clientId;
@@ -1386,11 +1380,18 @@ function addMeOAuthClientsSubcommand(me) {
1386
1380
  printSuccess("OAuth client created.");
1387
1381
  })
1388
1382
  );
1389
- addNotes(create, SCOPES_HELP_NOTES);
1383
+ addNotes(create, [
1384
+ "Use --policy to attach an existing XACML policy to the OAuth client.",
1385
+ "Manage policies with `geonic admin policies` commands."
1386
+ ]);
1390
1387
  addExamples(create, [
1391
1388
  {
1392
1389
  description: "Create an OAuth client with flags",
1393
- command: "geonic me oauth-clients create --name my-ci-bot --scopes read:entities,write:entities"
1390
+ command: "geonic me oauth-clients create --name my-ci-bot"
1391
+ },
1392
+ {
1393
+ description: "Create with a policy attached",
1394
+ command: "geonic me oauth-clients create --name my-ci-bot --policy <policy-id>"
1394
1395
  },
1395
1396
  {
1396
1397
  description: "Create and save credentials for auto-reauth",
@@ -1398,7 +1399,53 @@ function addMeOAuthClientsSubcommand(me) {
1398
1399
  },
1399
1400
  {
1400
1401
  description: "Create an OAuth client from JSON",
1401
- command: `geonic me oauth-clients create '{"clientName":"my-bot","allowedScopes":["read:entities"]}'`
1402
+ command: `geonic me oauth-clients create '{"name":"my-bot","policyId":"<policy-id>"}'`
1403
+ }
1404
+ ]);
1405
+ const update = oauthClients.command("update <clientId> [json]").description("Update an OAuth client").option("--name <name>", "Client name").option("--description <desc>", "Client description").option("--policy-id <policyId>", "Policy ID to attach (use 'null' to unbind)").option("--active", "Activate the OAuth client").option("--inactive", "Deactivate the OAuth client").action(
1406
+ withErrorHandler(async (clientId, json, _opts, cmd) => {
1407
+ const opts = cmd.opts();
1408
+ let body;
1409
+ if (json) {
1410
+ body = await parseJsonInput(json);
1411
+ } else if (opts.name || opts.description || opts.policyId !== void 0 || opts.active || opts.inactive) {
1412
+ const payload = {};
1413
+ if (opts.name) payload.name = opts.name;
1414
+ if (opts.description) payload.description = opts.description;
1415
+ if (opts.policyId !== void 0) payload.policyId = opts.policyId === "null" ? null : opts.policyId;
1416
+ if (opts.active) payload.isActive = true;
1417
+ if (opts.inactive) payload.isActive = false;
1418
+ body = payload;
1419
+ } else {
1420
+ body = await parseJsonInput();
1421
+ }
1422
+ const client = createClient(cmd);
1423
+ const format = getFormat(cmd);
1424
+ const response = await client.rawRequest(
1425
+ "PATCH",
1426
+ `/me/oauth-clients/${encodeURIComponent(String(clientId))}`,
1427
+ { body }
1428
+ );
1429
+ outputResponse(response, format);
1430
+ printSuccess("OAuth client updated.");
1431
+ })
1432
+ );
1433
+ addExamples(update, [
1434
+ {
1435
+ description: "Rename an OAuth client",
1436
+ command: "geonic me oauth-clients update <client-id> --name new-name"
1437
+ },
1438
+ {
1439
+ description: "Attach a policy",
1440
+ command: "geonic me oauth-clients update <client-id> --policy-id <policy-id>"
1441
+ },
1442
+ {
1443
+ description: "Unbind policy",
1444
+ command: "geonic me oauth-clients update <client-id> --policy-id null"
1445
+ },
1446
+ {
1447
+ description: "Deactivate an OAuth client",
1448
+ command: "geonic me oauth-clients update <client-id> --inactive"
1402
1449
  }
1403
1450
  ]);
1404
1451
  const del = oauthClients.command("delete <id>").description("Delete an OAuth client").action(
@@ -1417,9 +1464,67 @@ function addMeOAuthClientsSubcommand(me) {
1417
1464
  command: "geonic me oauth-clients delete <client-id>"
1418
1465
  }
1419
1466
  ]);
1467
+ const regenerateSecret = oauthClients.command("regenerate-secret <clientId>").description("Regenerate the client secret of an OAuth client").action(
1468
+ withErrorHandler(async (clientId, _opts, cmd) => {
1469
+ const client = createClient(cmd);
1470
+ const format = getFormat(cmd);
1471
+ const response = await client.rawRequest(
1472
+ "POST",
1473
+ `/me/oauth-clients/${encodeURIComponent(String(clientId))}/regenerate-secret`
1474
+ );
1475
+ printWarning("Save the new clientSecret now \u2014 it will not be shown again.");
1476
+ outputResponse(response, format);
1477
+ printSuccess("OAuth client secret regenerated.");
1478
+ })
1479
+ );
1480
+ addExamples(regenerateSecret, [
1481
+ {
1482
+ description: "Regenerate client secret",
1483
+ command: "geonic me oauth-clients regenerate-secret <client-id>"
1484
+ }
1485
+ ]);
1420
1486
  }
1421
1487
 
1422
1488
  // src/commands/me-api-keys.ts
1489
+ function cleanApiKeyData(data) {
1490
+ if (Array.isArray(data)) return data.map(cleanApiKeyData);
1491
+ if (typeof data !== "object" || data === null) return data;
1492
+ const obj = { ...data };
1493
+ if (obj.key === "******") delete obj.key;
1494
+ return obj;
1495
+ }
1496
+ function handleSaveKey(data, cmd) {
1497
+ const globalOpts = resolveOptions(cmd);
1498
+ const key = data.key;
1499
+ if (!key) {
1500
+ printError("Response missing key. API key was created, but it could not be saved.");
1501
+ process.exitCode = 1;
1502
+ return false;
1503
+ }
1504
+ try {
1505
+ const config = loadConfig(globalOpts.profile);
1506
+ config.apiKey = key;
1507
+ saveConfig(config, globalOpts.profile);
1508
+ console.error("API key saved to config. X-Api-Key header will be sent automatically.");
1509
+ return true;
1510
+ } catch (err) {
1511
+ printError(`Failed to save API key to config: ${err instanceof Error ? err.message : String(err)}`);
1512
+ printApiKeyBox(key);
1513
+ process.exitCode = 1;
1514
+ return false;
1515
+ }
1516
+ }
1517
+ function showKeyResult(data, save, cmd) {
1518
+ const key = data.key;
1519
+ if (!key) {
1520
+ printError("Response missing key. The new API key value was not returned.");
1521
+ process.exitCode = 1;
1522
+ return false;
1523
+ }
1524
+ if (save) return handleSaveKey(data, cmd);
1525
+ printApiKeyBox(key);
1526
+ return true;
1527
+ }
1423
1528
  function addMeApiKeysSubcommand(me) {
1424
1529
  const apiKeys = me.command("api-keys").description("Manage your API keys");
1425
1530
  const list = apiKeys.command("list").description("List your API keys").action(
@@ -1427,7 +1532,9 @@ function addMeApiKeysSubcommand(me) {
1427
1532
  const client = createClient(cmd);
1428
1533
  const format = getFormat(cmd);
1429
1534
  const response = await client.rawRequest("GET", "/me/api-keys");
1535
+ response.data = cleanApiKeyData(response.data);
1430
1536
  outputResponse(response, format);
1537
+ console.error("\u203B API \u30AD\u30FC\u5024\u306F\u4F5C\u6210\u6642 (create) \u307E\u305F\u306F\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u6642 (refresh) \u306B\u306E\u307F\u8868\u793A\u3055\u308C\u307E\u3059\u3002");
1431
1538
  })
1432
1539
  );
1433
1540
  addExamples(list, [
@@ -1436,7 +1543,7 @@ function addMeApiKeysSubcommand(me) {
1436
1543
  command: "geonic me api-keys list"
1437
1544
  }
1438
1545
  ]);
1439
- 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("--dpop-required", "Require DPoP token binding").option("--permissions <perms>", "Comma-separated permissions (read, write, create, update, delete)").option("--save", "Save the API key to config for automatic use").action(
1546
+ const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--policy <policyId>", "Policy ID to attach").option("--origins <origins>", "Allowed origins (comma-separated)").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--save", "Save the API key to config for automatic use").action(
1440
1547
  withErrorHandler(async (json, _opts, cmd) => {
1441
1548
  const opts = cmd.opts();
1442
1549
  if (opts.origins !== void 0) {
@@ -1449,14 +1556,12 @@ function addMeApiKeysSubcommand(me) {
1449
1556
  let body;
1450
1557
  if (json) {
1451
1558
  body = await parseJsonInput(json);
1452
- } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.permissions) {
1559
+ } else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0) {
1453
1560
  const payload = {};
1454
1561
  if (opts.name) payload.name = opts.name;
1455
- if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
1562
+ if (opts.policy) payload.policyId = opts.policy;
1456
1563
  if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
1457
- if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
1458
1564
  if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
1459
- if (opts.permissions) payload.permissions = parsePermissions(opts.permissions);
1460
1565
  if (opts.rateLimit) {
1461
1566
  const raw = opts.rateLimit.trim();
1462
1567
  if (!/^\d+$/.test(raw)) {
@@ -1485,41 +1590,19 @@ function addMeApiKeysSubcommand(me) {
1485
1590
  const format = getFormat(cmd);
1486
1591
  const response = await client.rawRequest("POST", "/me/api-keys", { body });
1487
1592
  const data = response.data;
1488
- if (opts.save) {
1489
- const globalOpts = resolveOptions(cmd);
1490
- const key = data.key;
1491
- if (!key) {
1492
- printError("Response missing key. API key was created, but it could not be saved.");
1493
- outputResponse(response, format);
1494
- process.exitCode = 1;
1495
- return;
1496
- }
1497
- const config = loadConfig(globalOpts.profile);
1498
- config.apiKey = key;
1499
- saveConfig(config, globalOpts.profile);
1500
- console.error("API key saved to config. X-Api-Key header will be sent automatically.");
1501
- } else {
1502
- printWarning("Save the API key now \u2014 it will not be shown again. Use --save to store it automatically.");
1503
- }
1593
+ const ok = showKeyResult(data, !!opts.save, cmd);
1504
1594
  outputResponse(response, format);
1505
- console.error("API key created.");
1595
+ if (ok) console.error("API key created.");
1506
1596
  })
1507
1597
  );
1508
1598
  addNotes(create, [
1509
- ...API_KEY_SCOPES_HELP_NOTES,
1510
- "",
1511
- "Valid permissions: read, write, create, update, delete",
1512
- " write = create + update + delete",
1513
- " Permissions auto-generate XACML policies (allowedEntityTypes respected)."
1599
+ "Use --policy to attach an existing XACML policy to the API key.",
1600
+ "Manage policies with `geonic admin policies` commands."
1514
1601
  ]);
1515
1602
  addExamples(create, [
1516
1603
  {
1517
- description: "Create an API key with flags",
1518
- command: "geonic me api-keys create --name my-app --scopes read:entities --origins 'https://example.com'"
1519
- },
1520
- {
1521
- description: "Create with permissions (auto-generates XACML policy)",
1522
- command: "geonic me api-keys create --name my-app --permissions read,write --save"
1604
+ description: "Create an API key with a policy",
1605
+ command: "geonic me api-keys create --name my-app --policy <policy-id>"
1523
1606
  },
1524
1607
  {
1525
1608
  description: "Create and save API key to config",
@@ -1527,7 +1610,7 @@ function addMeApiKeysSubcommand(me) {
1527
1610
  },
1528
1611
  {
1529
1612
  description: "Create an API key from JSON",
1530
- command: `geonic me api-keys create '{"name":"my-app","allowedScopes":["read:entities"]}'`
1613
+ command: `geonic me api-keys create '{"name":"my-app","policyId":"<policy-id>"}'`
1531
1614
  },
1532
1615
  {
1533
1616
  description: "Create an API key with rate limiting",
@@ -1538,6 +1621,106 @@ function addMeApiKeysSubcommand(me) {
1538
1621
  command: "geonic me api-keys create --name my-app --dpop-required"
1539
1622
  }
1540
1623
  ]);
1624
+ const refresh = apiKeys.command("refresh <keyId>").description("Refresh (rotate) an API key \u2014 generates a new key value").option("--save", "Save the new API key to config for automatic use").action(
1625
+ withErrorHandler(async (keyId, _opts, cmd) => {
1626
+ const opts = cmd.opts();
1627
+ const client = createClient(cmd);
1628
+ const format = getFormat(cmd);
1629
+ const response = await client.rawRequest(
1630
+ "POST",
1631
+ `/me/api-keys/${encodeURIComponent(String(keyId))}/refresh`
1632
+ );
1633
+ const data = response.data;
1634
+ const ok = showKeyResult(data, !!opts.save, cmd);
1635
+ outputResponse(response, format);
1636
+ if (ok) console.error("API key refreshed.");
1637
+ })
1638
+ );
1639
+ addNotes(refresh, [
1640
+ "Refreshing generates a new key value while keeping keyId, name, and policy settings.",
1641
+ "The previous key value is immediately invalidated."
1642
+ ]);
1643
+ addExamples(refresh, [
1644
+ {
1645
+ description: "Refresh an API key",
1646
+ command: "geonic me api-keys refresh <key-id>"
1647
+ },
1648
+ {
1649
+ description: "Refresh and save new key to config",
1650
+ command: "geonic me api-keys refresh <key-id> --save"
1651
+ }
1652
+ ]);
1653
+ const update = apiKeys.command("update <keyId> [json]").description("Update an API key").option("--name <name>", "Key name").option("--policy-id <policyId>", "Policy ID to attach (use 'null' to unbind)").option("--origins <origins>", "Allowed origins (comma-separated)").option("--rate-limit <n>", "Rate limit (requests per minute)").option("--dpop-required", "Require DPoP token binding").option("--no-dpop-required", "Disable DPoP requirement").option("--active", "Activate the API key").option("--inactive", "Deactivate the API key").action(
1654
+ withErrorHandler(async (keyId, json, _opts, cmd) => {
1655
+ const opts = cmd.opts();
1656
+ if (opts.origins !== void 0) {
1657
+ const parsed = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
1658
+ if (parsed.length === 0) {
1659
+ printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
1660
+ process.exit(1);
1661
+ }
1662
+ }
1663
+ let body;
1664
+ if (json) {
1665
+ body = await parseJsonInput(json);
1666
+ } else if (opts.name || opts.policyId !== void 0 || opts.origins !== void 0 || opts.rateLimit || opts.dpopRequired !== void 0 || opts.active || opts.inactive) {
1667
+ const payload = {};
1668
+ if (opts.name) payload.name = opts.name;
1669
+ if (opts.policyId !== void 0) payload.policyId = opts.policyId === "null" ? null : opts.policyId;
1670
+ if (opts.origins !== void 0) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
1671
+ if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
1672
+ if (opts.rateLimit) {
1673
+ const raw = opts.rateLimit.trim();
1674
+ if (!/^\d+$/.test(raw)) {
1675
+ printError("--rate-limit must be a positive integer.");
1676
+ process.exit(1);
1677
+ }
1678
+ const perMinute = Number(raw);
1679
+ if (perMinute <= 0) {
1680
+ printError("--rate-limit must be a positive integer.");
1681
+ process.exit(1);
1682
+ }
1683
+ payload.rateLimit = { perMinute };
1684
+ }
1685
+ if (opts.active) payload.isActive = true;
1686
+ if (opts.inactive) payload.isActive = false;
1687
+ body = payload;
1688
+ } else {
1689
+ body = await parseJsonInput();
1690
+ }
1691
+ const client = createClient(cmd);
1692
+ const format = getFormat(cmd);
1693
+ const response = await client.rawRequest(
1694
+ "PATCH",
1695
+ `/me/api-keys/${encodeURIComponent(String(keyId))}`,
1696
+ { body }
1697
+ );
1698
+ outputResponse(response, format);
1699
+ console.error("API key updated.");
1700
+ })
1701
+ );
1702
+ addExamples(update, [
1703
+ {
1704
+ description: "Rename an API key",
1705
+ command: "geonic me api-keys update <key-id> --name new-name"
1706
+ },
1707
+ {
1708
+ description: "Attach a policy",
1709
+ command: "geonic me api-keys update <key-id> --policy-id <policy-id>"
1710
+ },
1711
+ {
1712
+ description: "Unbind policy",
1713
+ command: "geonic me api-keys update <key-id> --policy-id null"
1714
+ },
1715
+ {
1716
+ description: "Deactivate an API key",
1717
+ command: "geonic me api-keys update <key-id> --inactive"
1718
+ },
1719
+ {
1720
+ description: "Update from JSON",
1721
+ command: `geonic me api-keys update <key-id> '{"name":"new-name","rateLimit":{"perMinute":60}}'`
1722
+ }
1723
+ ]);
1541
1724
  const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
1542
1725
  withErrorHandler(async (keyId, _opts, cmd) => {
1543
1726
  const client = createClient(cmd);
@@ -1556,6 +1739,149 @@ function addMeApiKeysSubcommand(me) {
1556
1739
  ]);
1557
1740
  }
1558
1741
 
1742
+ // src/commands/me-policies.ts
1743
+ function addMePoliciesSubcommand(me) {
1744
+ const policies = me.command("policies").description("Manage your personal XACML policies");
1745
+ const list = policies.command("list").description("List your personal policies").action(
1746
+ withErrorHandler(async (_opts, cmd) => {
1747
+ const client = createClient(cmd);
1748
+ const format = getFormat(cmd);
1749
+ const response = await client.rawRequest("GET", "/me/policies");
1750
+ outputResponse(response, format);
1751
+ })
1752
+ );
1753
+ addExamples(list, [
1754
+ {
1755
+ description: "List your personal policies",
1756
+ command: "geonic me policies list"
1757
+ }
1758
+ ]);
1759
+ const get = policies.command("get <policyId>").description("Get a personal policy by ID").action(
1760
+ withErrorHandler(async (policyId, _opts, cmd) => {
1761
+ const client = createClient(cmd);
1762
+ const format = getFormat(cmd);
1763
+ const response = await client.rawRequest(
1764
+ "GET",
1765
+ `/me/policies/${encodeURIComponent(String(policyId))}`
1766
+ );
1767
+ outputResponse(response, format);
1768
+ })
1769
+ );
1770
+ addExamples(get, [
1771
+ {
1772
+ description: "Get a personal policy by ID",
1773
+ command: "geonic me policies get <policy-id>"
1774
+ }
1775
+ ]);
1776
+ const create = policies.command("create [json]").description(
1777
+ `Create a personal XACML policy
1778
+
1779
+ Constraints (enforced server-side):
1780
+ - priority is fixed at 100 (user role minimum)
1781
+ - scope is 'personal' \u2014 not applied tenant-wide
1782
+ - target is required
1783
+ - data API paths only (/v2/**, /ngsi-ld/** etc.)
1784
+
1785
+ Example \u2014 GET-only policy for /v2/**:
1786
+ {
1787
+ "policyId": "my-readonly",
1788
+ "target": {
1789
+ "resources": [{"attributeId": "path", "matchValue": "/v2/**", "matchFunction": "glob"}]
1790
+ },
1791
+ "rules": [
1792
+ {"ruleId": "allow-get", "effect": "Permit", "target": {"actions": [{"attributeId": "method", "matchValue": "GET"}]}},
1793
+ {"ruleId": "deny-others", "effect": "Deny"}
1794
+ ]
1795
+ }`
1796
+ ).option("--policy-id <id>", "Policy ID (auto-generated UUID if omitted)").option("--description <text>", "Policy description").action(
1797
+ withErrorHandler(async (json, _opts, cmd) => {
1798
+ const opts = cmd.opts();
1799
+ let body;
1800
+ if (json) {
1801
+ body = await parseJsonInput(json);
1802
+ } else if (opts.policyId || opts.description) {
1803
+ const payload = {};
1804
+ if (opts.policyId) payload.policyId = opts.policyId;
1805
+ if (opts.description) payload.description = opts.description;
1806
+ body = payload;
1807
+ } else {
1808
+ body = await parseJsonInput();
1809
+ }
1810
+ const client = createClient(cmd);
1811
+ const format = getFormat(cmd);
1812
+ const response = await client.rawRequest("POST", "/me/policies", { body });
1813
+ outputResponse(response, format);
1814
+ printSuccess("Policy created.");
1815
+ })
1816
+ );
1817
+ addNotes(create, [
1818
+ "priority is always set to 100 by the server regardless of the value you specify.",
1819
+ "Bind the policy to an API key or OAuth client with `geonic me api-keys update --policy-id` or `geonic me oauth-clients update --policy-id`."
1820
+ ]);
1821
+ addExamples(create, [
1822
+ {
1823
+ description: "Create a GET-only policy from inline JSON",
1824
+ command: `geonic me policies create '{"policyId":"my-readonly","target":{"resources":[{"attributeId":"path","matchValue":"/v2/**","matchFunction":"glob"}]},"rules":[{"ruleId":"allow-get","effect":"Permit","target":{"actions":[{"attributeId":"method","matchValue":"GET"}]}},{"ruleId":"deny-others","effect":"Deny"}]}'`
1825
+ },
1826
+ {
1827
+ description: "Create from a JSON file",
1828
+ command: "geonic me policies create @policy.json"
1829
+ },
1830
+ {
1831
+ description: "Create from stdin",
1832
+ command: "cat policy.json | geonic me policies create"
1833
+ }
1834
+ ]);
1835
+ const update = policies.command("update <policyId> [json]").description("Update a personal policy (partial update)").option("--description <text>", "Policy description").action(
1836
+ withErrorHandler(async (policyId, json, _opts, cmd) => {
1837
+ const opts = cmd.opts();
1838
+ let body;
1839
+ if (json) {
1840
+ body = await parseJsonInput(json);
1841
+ } else if (opts.description) {
1842
+ body = { description: opts.description };
1843
+ } else {
1844
+ body = await parseJsonInput();
1845
+ }
1846
+ const client = createClient(cmd);
1847
+ const format = getFormat(cmd);
1848
+ const response = await client.rawRequest(
1849
+ "PATCH",
1850
+ `/me/policies/${encodeURIComponent(String(policyId))}`,
1851
+ { body }
1852
+ );
1853
+ outputResponse(response, format);
1854
+ printSuccess("Policy updated.");
1855
+ })
1856
+ );
1857
+ addExamples(update, [
1858
+ {
1859
+ description: "Update policy rules",
1860
+ command: `geonic me policies update <policy-id> '{"rules":[{"ruleId":"allow-get","effect":"Permit"}]}'`
1861
+ },
1862
+ {
1863
+ description: "Update from a JSON file",
1864
+ command: "geonic me policies update <policy-id> @patch.json"
1865
+ }
1866
+ ]);
1867
+ const del = policies.command("delete <policyId>").description("Delete a personal policy").action(
1868
+ withErrorHandler(async (policyId, _opts, cmd) => {
1869
+ const client = createClient(cmd);
1870
+ await client.rawRequest(
1871
+ "DELETE",
1872
+ `/me/policies/${encodeURIComponent(String(policyId))}`
1873
+ );
1874
+ printSuccess("Policy deleted.");
1875
+ })
1876
+ );
1877
+ addExamples(del, [
1878
+ {
1879
+ description: "Delete a personal policy",
1880
+ command: "geonic me policies delete <policy-id>"
1881
+ }
1882
+ ]);
1883
+ }
1884
+
1559
1885
  // src/commands/auth.ts
1560
1886
  function createLoginCommand() {
1561
1887
  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(
@@ -1624,10 +1950,10 @@ function createLoginCommand() {
1624
1950
  process.exit(1);
1625
1951
  }
1626
1952
  const availableTenants = data.availableTenants;
1627
- const currentTenantId = data.tenantId;
1953
+ let finalTenantId = data.tenantId;
1628
1954
  if (availableTenants && availableTenants.length > 1 && !loginOpts.tenantId) {
1629
- const selectedTenantId = await promptTenantSelection(availableTenants, currentTenantId);
1630
- if (selectedTenantId && selectedTenantId !== currentTenantId) {
1955
+ const selectedTenantId = await promptTenantSelection(availableTenants, finalTenantId);
1956
+ if (selectedTenantId && selectedTenantId !== finalTenantId) {
1631
1957
  const reloginResponse = await client.rawRequest("POST", "/auth/login", {
1632
1958
  body: { email, password, tenantId: selectedTenantId },
1633
1959
  skipTenantHeader: true
@@ -1640,6 +1966,7 @@ function createLoginCommand() {
1640
1966
  }
1641
1967
  token = newToken;
1642
1968
  refreshToken = reloginData.refreshToken;
1969
+ finalTenantId = selectedTenantId;
1643
1970
  }
1644
1971
  }
1645
1972
  const config = loadConfig(globalOpts.profile);
@@ -1649,6 +1976,11 @@ function createLoginCommand() {
1649
1976
  } else {
1650
1977
  delete config.refreshToken;
1651
1978
  }
1979
+ if (finalTenantId) {
1980
+ config.service = finalTenantId;
1981
+ } else {
1982
+ delete config.service;
1983
+ }
1652
1984
  saveConfig(config, globalOpts.profile);
1653
1985
  printSuccess("Login successful. Token saved to config.");
1654
1986
  })
@@ -1887,6 +2219,7 @@ function registerAuthCommands(program2) {
1887
2219
  ]);
1888
2220
  addMeOAuthClientsSubcommand(me);
1889
2221
  addMeApiKeysSubcommand(me);
2222
+ addMePoliciesSubcommand(me);
1890
2223
  program2.addCommand(createLoginCommand(), { hidden: true });
1891
2224
  program2.addCommand(createLogoutCommand(), { hidden: true });
1892
2225
  const hiddenWhoami = new Command("whoami").description("Display current authenticated user").action(createMeAction());
@@ -3386,7 +3719,7 @@ function registerPoliciesCommand(parent) {
3386
3719
  }
3387
3720
  ]);
3388
3721
  const create = policies.command("create [json]").description(
3389
- 'Create a new policy\n\nJSON payload examples:\n\n Allow all entities:\n {\n "description": "Allow all entities",\n "rules": [{"ruleId": "allow-all", "effect": "Permit"}]\n }\n\n Allow GET access to a specific entity type:\n {\n "description": "Allow GET access to Landmark entities",\n "target": {\n "resources": [{"attributeId": "entityType", "matchValue": "Landmark"}],\n "actions": [{"attributeId": "method", "matchValue": "GET"}]\n },\n "rules": [{"ruleId": "permit-get", "effect": "Permit"}]\n }\n\nTarget fields:\n subjects \u2014 attributeId: role, userId, email, tenantId\n resources \u2014 attributeId: path, entityType, entityId, entityOwner, tenantService, servicePath\n actions \u2014 attributeId: method (GET, POST, PATCH, DELETE)\n\nEach element: {attributeId, matchValue, matchFunction?}\n matchFunction: "string-equal" (default) | "string-regexp" | "glob"\n\nPriority: higher number = higher priority (default: 0).\n Custom policies (e.g. 100) override default role policies (0).\n\nDefault role policies (priority 0):\n user \u2192 GET only, api_key \u2192 all Deny, anonymous \u2192 all Deny'
3722
+ 'Create a new policy\n\nJSON payload examples:\n\n Allow all entities:\n {\n "description": "Allow all entities",\n "rules": [{"ruleId": "allow-all", "effect": "Permit"}]\n }\n\n Allow GET access to a specific entity type:\n {\n "description": "Allow GET access to Landmark entities",\n "target": {\n "resources": [{"attributeId": "entityType", "matchValue": "Landmark"}],\n "actions": [{"attributeId": "method", "matchValue": "GET"}]\n },\n "rules": [{"ruleId": "permit-get", "effect": "Permit"}]\n }\n\nTarget fields:\n subjects \u2014 attributeId: role, userId, email, tenantId\n resources \u2014 attributeId: path, entityType, entityId, entityOwner, tenantService, servicePath\n actions \u2014 attributeId: method (GET, POST, PATCH, DELETE)\n\nEach element: {attributeId, matchValue, matchFunction?}\n matchFunction: "string-equal" (default) | "string-regexp" | "glob"\n\nPriority: smaller value = higher precedence (e.g. priority 10 overrides user default at 100).\n tenant_admin: minimum priority 10. user self-service (/me/policies): fixed at 100.\n\nDefault role policies (priority 100):\n user \u2192 /v2/** and /ngsi-ld/** all methods Permit; other data APIs GET only\n api_key \u2192 all Deny, anonymous \u2192 all Deny'
3390
3723
  ).action(
3391
3724
  withErrorHandler(async (json, _opts, cmd) => {
3392
3725
  const body = await parseJsonInput(json);
@@ -3542,7 +3875,7 @@ function registerOAuthClientsCommand(parent) {
3542
3875
  }
3543
3876
  ]);
3544
3877
  const create = oauthClients.command("create [json]").description(
3545
- 'Create a new OAuth client\n\nJSON payload example:\n {\n "clientName": "my-app",\n "allowedScopes": ["read:entities", "write:entities"]\n }'
3878
+ 'Create a new OAuth client\n\nJSON payload example:\n {\n "name": "my-app",\n "policyId": "<policy-id>"\n }'
3546
3879
  ).action(
3547
3880
  withErrorHandler(async (json, _opts, cmd) => {
3548
3881
  const body = await parseJsonInput(json);
@@ -3558,7 +3891,7 @@ function registerOAuthClientsCommand(parent) {
3558
3891
  addExamples(create, [
3559
3892
  {
3560
3893
  description: "Create with inline JSON",
3561
- command: `geonic admin oauth-clients create '{"clientName":"my-app","allowedScopes":["read:entities","write:entities"]}'`
3894
+ command: `geonic admin oauth-clients create '{"name":"my-app","policyId":"<policy-id>"}'`
3562
3895
  },
3563
3896
  {
3564
3897
  description: "Create from a JSON file",
@@ -3678,6 +4011,13 @@ function registerCaddeCommand(parent) {
3678
4011
  }
3679
4012
 
3680
4013
  // src/commands/admin/api-keys.ts
4014
+ function cleanApiKeyData2(data) {
4015
+ if (Array.isArray(data)) return data.map(cleanApiKeyData2);
4016
+ if (typeof data !== "object" || data === null) return data;
4017
+ const obj = { ...data };
4018
+ if (obj.key === "******") delete obj.key;
4019
+ return obj;
4020
+ }
3681
4021
  function validateOrigins(body, opts) {
3682
4022
  if (opts.origins !== void 0) {
3683
4023
  const origins = String(opts.origins).split(",").map((s) => s.trim()).filter(Boolean);
@@ -3697,9 +4037,8 @@ function validateOrigins(body, opts) {
3697
4037
  function buildBodyFromFlags(opts) {
3698
4038
  const payload = {};
3699
4039
  if (opts.name) payload.name = opts.name;
3700
- if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
4040
+ if (opts.policy) payload.policyId = opts.policy;
3701
4041
  if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
3702
- if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
3703
4042
  if (opts.rateLimit) {
3704
4043
  const raw = String(opts.rateLimit).trim();
3705
4044
  if (!/^\d+$/.test(raw)) {
@@ -3714,10 +4053,41 @@ function buildBodyFromFlags(opts) {
3714
4053
  payload.rateLimit = { perMinute };
3715
4054
  }
3716
4055
  if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
3717
- if (opts.permissions) payload.permissions = parsePermissions(opts.permissions);
3718
4056
  if (opts.tenantId) payload.tenantId = opts.tenantId;
3719
4057
  return payload;
3720
4058
  }
4059
+ function handleSaveKey2(data, cmd) {
4060
+ const globalOpts = resolveOptions(cmd);
4061
+ const key = data.key;
4062
+ if (!key) {
4063
+ printError("Response missing key. API key was created, but it could not be saved.");
4064
+ process.exitCode = 1;
4065
+ return false;
4066
+ }
4067
+ try {
4068
+ const config = loadConfig(globalOpts.profile);
4069
+ config.apiKey = key;
4070
+ saveConfig(config, globalOpts.profile);
4071
+ console.error("API key saved to config. X-Api-Key header will be sent automatically.");
4072
+ return true;
4073
+ } catch (err) {
4074
+ printError(`Failed to save API key to config: ${err instanceof Error ? err.message : String(err)}`);
4075
+ printApiKeyBox(key);
4076
+ process.exitCode = 1;
4077
+ return false;
4078
+ }
4079
+ }
4080
+ function showKeyResult2(data, save, cmd) {
4081
+ const key = data.key;
4082
+ if (!key) {
4083
+ printError("Response missing key. The new API key value was not returned.");
4084
+ process.exitCode = 1;
4085
+ return false;
4086
+ }
4087
+ if (save) return handleSaveKey2(data, cmd);
4088
+ printApiKeyBox(key);
4089
+ return true;
4090
+ }
3721
4091
  function registerApiKeysCommand(parent) {
3722
4092
  const apiKeys = parent.command("api-keys").description("Manage API keys");
3723
4093
  const list = apiKeys.command("list").description("List all API keys").option("--tenant-id <id>", "Filter by tenant ID").action(
@@ -3730,7 +4100,9 @@ function registerApiKeysCommand(parent) {
3730
4100
  const response = await client.rawRequest("GET", "/admin/api-keys", {
3731
4101
  params
3732
4102
  });
4103
+ response.data = cleanApiKeyData2(response.data);
3733
4104
  outputResponse(response, format);
4105
+ console.error("\u203B API \u30AD\u30FC\u5024\u306F\u4F5C\u6210\u6642 (create) \u307E\u305F\u306F\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u6642 (refresh) \u306B\u306E\u307F\u8868\u793A\u3055\u308C\u307E\u3059\u3002");
3734
4106
  })
3735
4107
  );
3736
4108
  addExamples(list, [
@@ -3751,6 +4123,7 @@ function registerApiKeysCommand(parent) {
3751
4123
  "GET",
3752
4124
  `/admin/api-keys/${encodeURIComponent(String(keyId))}`
3753
4125
  );
4126
+ response.data = cleanApiKeyData2(response.data);
3754
4127
  outputResponse(response, format);
3755
4128
  })
3756
4129
  );
@@ -3760,14 +4133,14 @@ function registerApiKeysCommand(parent) {
3760
4133
  command: "geonic admin api-keys get <key-id>"
3761
4134
  }
3762
4135
  ]);
3763
- 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("--dpop-required", "Require DPoP token binding").option("--permissions <perms>", "Comma-separated permissions (read, write, create, update, delete)").option("--tenant-id <id>", "Tenant ID").option("--save", "Save the API key to profile config").action(
4136
+ const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--policy <policyId>", "Policy ID to attach").option("--origins <origins>", "Comma-separated origins").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--tenant-id <id>", "Tenant ID").option("--save", "Save the API key to profile config").action(
3764
4137
  withErrorHandler(async (json, _opts, cmd) => {
3765
4138
  const opts = cmd.opts();
3766
4139
  validateOrigins(void 0, opts);
3767
4140
  let body;
3768
4141
  if (json) {
3769
4142
  body = await parseJsonInput(json);
3770
- } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.permissions || opts.tenantId) {
4143
+ } else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0 || opts.tenantId) {
3771
4144
  body = buildBodyFromFlags(opts);
3772
4145
  } else {
3773
4146
  body = await parseJsonInput();
@@ -3779,41 +4152,19 @@ function registerApiKeysCommand(parent) {
3779
4152
  body
3780
4153
  });
3781
4154
  const data = response.data;
3782
- if (opts.save) {
3783
- const globalOpts = resolveOptions(cmd);
3784
- const key = data.key;
3785
- if (!key) {
3786
- printError("Response missing key. API key was created, but it could not be saved.");
3787
- outputResponse(response, format);
3788
- process.exitCode = 1;
3789
- return;
3790
- }
3791
- const config = loadConfig(globalOpts.profile);
3792
- config.apiKey = key;
3793
- saveConfig(config, globalOpts.profile);
3794
- console.error("API key saved to config. X-Api-Key header will be sent automatically.");
3795
- } else {
3796
- printWarning("Save the API key now \u2014 it will not be shown again. Use --save to store it automatically.");
3797
- }
4155
+ const ok = showKeyResult2(data, !!opts.save, cmd);
3798
4156
  outputResponse(response, format);
3799
- console.error("API key created.");
4157
+ if (ok) console.error("API key created.");
3800
4158
  })
3801
4159
  );
3802
4160
  addNotes(create, [
3803
- ...API_KEY_SCOPES_HELP_NOTES,
3804
- "",
3805
- "Valid permissions: read, write, create, update, delete",
3806
- " write = create + update + delete",
3807
- " Permissions auto-generate XACML policies (allowedEntityTypes respected)."
4161
+ "Use --policy to attach an existing XACML policy to the API key.",
4162
+ "Manage policies with `geonic admin policies` commands."
3808
4163
  ]);
3809
4164
  addExamples(create, [
3810
4165
  {
3811
- description: "Create an API key with flags",
3812
- command: "geonic admin api-keys create --name my-key --scopes read:entities,write:entities --origins '*'"
3813
- },
3814
- {
3815
- description: "Create with permissions (auto-generates XACML policy)",
3816
- command: "geonic admin api-keys create --name my-key --permissions read,write --origins '*'"
4166
+ description: "Create an API key with a policy",
4167
+ command: "geonic admin api-keys create --name my-key --policy <policy-id> --origins '*'"
3817
4168
  },
3818
4169
  {
3819
4170
  description: "Create an API key with DPoP required",
@@ -3824,7 +4175,36 @@ function registerApiKeysCommand(parent) {
3824
4175
  command: "geonic admin api-keys create @key.json --save"
3825
4176
  }
3826
4177
  ]);
3827
- 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").option("--dpop-required", "Require DPoP token binding").option("--no-dpop-required", "Disable DPoP token binding").option("--permissions <perms>", "Comma-separated permissions (read, write, create, update, delete)").action(
4178
+ const refresh = apiKeys.command("refresh <keyId>").description("Refresh (rotate) an API key \u2014 generates a new key value").option("--save", "Save the new API key to profile config").action(
4179
+ withErrorHandler(async (keyId, _opts, cmd) => {
4180
+ const opts = cmd.opts();
4181
+ const client = createClient(cmd);
4182
+ const format = getFormat(cmd);
4183
+ const response = await client.rawRequest(
4184
+ "POST",
4185
+ `/admin/api-keys/${encodeURIComponent(String(keyId))}/refresh`
4186
+ );
4187
+ const data = response.data;
4188
+ const ok = showKeyResult2(data, !!opts.save, cmd);
4189
+ outputResponse(response, format);
4190
+ if (ok) console.error("API key refreshed.");
4191
+ })
4192
+ );
4193
+ addNotes(refresh, [
4194
+ "Refreshing generates a new key value while keeping keyId, name, and policy settings.",
4195
+ "The previous key value is immediately invalidated."
4196
+ ]);
4197
+ addExamples(refresh, [
4198
+ {
4199
+ description: "Refresh an API key",
4200
+ command: "geonic admin api-keys refresh <key-id>"
4201
+ },
4202
+ {
4203
+ description: "Refresh and save new key to config",
4204
+ command: "geonic admin api-keys refresh <key-id> --save"
4205
+ }
4206
+ ]);
4207
+ const update = apiKeys.command("update <keyId> [json]").description("Update an API key").option("--name <name>", "Key name").option("--policy <policyId>", "Policy ID to attach").option("--origins <origins>", "Comma-separated origins").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--no-dpop-required", "Disable DPoP token binding").action(
3828
4208
  withErrorHandler(
3829
4209
  async (keyId, json, _opts, cmd) => {
3830
4210
  const opts = cmd.opts();
@@ -3832,7 +4212,7 @@ function registerApiKeysCommand(parent) {
3832
4212
  let body;
3833
4213
  if (json) {
3834
4214
  body = await parseJsonInput(json);
3835
- } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.permissions) {
4215
+ } else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0) {
3836
4216
  body = buildBodyFromFlags(opts);
3837
4217
  } else {
3838
4218
  body = await parseJsonInput();
@@ -3851,11 +4231,8 @@ function registerApiKeysCommand(parent) {
3851
4231
  )
3852
4232
  );
3853
4233
  addNotes(update, [
3854
- ...API_KEY_SCOPES_HELP_NOTES,
3855
- "",
3856
- "Valid permissions: read, write, create, update, delete",
3857
- " write = create + update + delete",
3858
- " Permissions auto-generate XACML policies (allowedEntityTypes respected)."
4234
+ "Use --policy to attach an existing XACML policy to the API key.",
4235
+ "Manage policies with `geonic admin policies` commands."
3859
4236
  ]);
3860
4237
  addExamples(update, [
3861
4238
  {
@@ -3863,8 +4240,8 @@ function registerApiKeysCommand(parent) {
3863
4240
  command: "geonic admin api-keys update <key-id> --name new-name"
3864
4241
  },
3865
4242
  {
3866
- description: "Update permissions",
3867
- command: "geonic admin api-keys update <key-id> --permissions read,write"
4243
+ description: "Attach a policy",
4244
+ command: "geonic admin api-keys update <key-id> --policy <policy-id>"
3868
4245
  },
3869
4246
  {
3870
4247
  description: "Enable DPoP requirement",