@geolonia/geonicdb-cli 0.6.4 → 0.8.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
@@ -785,6 +785,15 @@ var GdbClient = class _GdbClient {
785
785
  canRefresh() {
786
786
  return (!!this.refreshToken || !!this.clientId && !!this.clientSecret) && !this.apiKey;
787
787
  }
788
+ /** Check whether an error indicates an authentication/token problem that may be resolved by refreshing. */
789
+ static isTokenError(err) {
790
+ if (err.status === 401) return true;
791
+ if (err.status === 403) {
792
+ const msg = (err.message ?? "").toLowerCase();
793
+ return msg.includes("not assigned to any tenant") || msg.includes("invalid token");
794
+ }
795
+ return false;
796
+ }
788
797
  async performTokenRefresh() {
789
798
  if (this.refreshPromise) return this.refreshPromise;
790
799
  this.refreshPromise = this.doRefresh();
@@ -888,7 +897,7 @@ var GdbClient = class _GdbClient {
888
897
  try {
889
898
  return await this.executeRequest(method, path, options);
890
899
  } catch (err) {
891
- if (err instanceof GdbClientError && err.status === 401 && this.canRefresh()) {
900
+ if (err instanceof GdbClientError && _GdbClient.isTokenError(err) && this.canRefresh()) {
892
901
  const refreshed = await this.performTokenRefresh();
893
902
  if (refreshed) {
894
903
  return await this.executeRequest(method, path, options);
@@ -917,7 +926,7 @@ var GdbClient = class _GdbClient {
917
926
  try {
918
927
  return await this.executeRawRequest(method, path, options);
919
928
  } catch (err) {
920
- if (err instanceof GdbClientError && err.status === 401 && this.canRefresh()) {
929
+ if (err instanceof GdbClientError && _GdbClient.isTokenError(err) && this.canRefresh()) {
921
930
  const refreshed = await this.performTokenRefresh();
922
931
  if (refreshed) {
923
932
  return await this.executeRawRequest(method, path, options);
@@ -937,21 +946,6 @@ var GdbClientError = class extends Error {
937
946
  };
938
947
 
939
948
  // 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
- "write:X implies read:X. admin:X implies both read:X and write:X."
949
- ];
950
- var API_KEY_SCOPES_HELP_NOTES = [
951
- "Valid scopes:",
952
- " read:entities, write:entities, read:subscriptions, write:subscriptions,",
953
- " read:registrations, write:registrations"
954
- ];
955
949
  function resolveOptions(cmd) {
956
950
  const opts = cmd.optsWithGlobals();
957
951
  const config = loadConfig(opts.profile);
@@ -1017,6 +1011,8 @@ function withErrorHandler(fn) {
1017
1011
  }
1018
1012
  if (err instanceof GdbClientError && err.status === 401) {
1019
1013
  printError("Authentication failed. Please re-authenticate (e.g., `geonic auth login` or check your API key).");
1014
+ } else if (err instanceof GdbClientError && err.status === 403 && /not assigned to any tenant|invalid token/i.test(err.message)) {
1015
+ printError("Authentication failed. Please re-authenticate (e.g., `geonic auth login` or check your API key).");
1020
1016
  } else if (err instanceof GdbClientError && err.status === 403) {
1021
1017
  const detail = (err.ngsiError?.detail ?? err.ngsiError?.description ?? "").toLowerCase();
1022
1018
  if (detail.includes("entity type") || detail.includes("allowedentitytypes")) {
@@ -1325,16 +1321,16 @@ function addMeOAuthClientsSubcommand(me) {
1325
1321
  command: "geonic me oauth-clients list"
1326
1322
  }
1327
1323
  ]);
1328
- 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(
1324
+ 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(
1329
1325
  withErrorHandler(async (json, _opts, cmd) => {
1330
1326
  const opts = cmd.opts();
1331
1327
  let body;
1332
1328
  if (json) {
1333
1329
  body = await parseJsonInput(json);
1334
- } else if (opts.name || opts.scopes) {
1330
+ } else if (opts.name || opts.policy) {
1335
1331
  const payload = {};
1336
- if (opts.name) payload.clientName = opts.name;
1337
- if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim());
1332
+ if (opts.name) payload.name = opts.name;
1333
+ if (opts.policy) payload.policyId = opts.policy;
1338
1334
  body = payload;
1339
1335
  } else {
1340
1336
  body = await parseJsonInput();
@@ -1357,8 +1353,7 @@ function addMeOAuthClientsSubcommand(me) {
1357
1353
  const tokenResult = await clientCredentialsGrant({
1358
1354
  baseUrl,
1359
1355
  clientId,
1360
- clientSecret,
1361
- scope: data.allowedScopes?.join(" ")
1356
+ clientSecret
1362
1357
  });
1363
1358
  const config = loadConfig(globalOpts.profile);
1364
1359
  config.clientId = clientId;
@@ -1376,11 +1371,18 @@ function addMeOAuthClientsSubcommand(me) {
1376
1371
  printSuccess("OAuth client created.");
1377
1372
  })
1378
1373
  );
1379
- addNotes(create, SCOPES_HELP_NOTES);
1374
+ addNotes(create, [
1375
+ "Use --policy to attach an existing XACML policy to the OAuth client.",
1376
+ "Manage policies with `geonic admin policies` commands."
1377
+ ]);
1380
1378
  addExamples(create, [
1381
1379
  {
1382
1380
  description: "Create an OAuth client with flags",
1383
- command: "geonic me oauth-clients create --name my-ci-bot --scopes read:entities,write:entities"
1381
+ command: "geonic me oauth-clients create --name my-ci-bot"
1382
+ },
1383
+ {
1384
+ description: "Create with a policy attached",
1385
+ command: "geonic me oauth-clients create --name my-ci-bot --policy <policy-id>"
1384
1386
  },
1385
1387
  {
1386
1388
  description: "Create and save credentials for auto-reauth",
@@ -1388,7 +1390,53 @@ function addMeOAuthClientsSubcommand(me) {
1388
1390
  },
1389
1391
  {
1390
1392
  description: "Create an OAuth client from JSON",
1391
- command: `geonic me oauth-clients create '{"clientName":"my-bot","allowedScopes":["read:entities"]}'`
1393
+ command: `geonic me oauth-clients create '{"name":"my-bot","policyId":"<policy-id>"}'`
1394
+ }
1395
+ ]);
1396
+ 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(
1397
+ withErrorHandler(async (clientId, json, _opts, cmd) => {
1398
+ const opts = cmd.opts();
1399
+ let body;
1400
+ if (json) {
1401
+ body = await parseJsonInput(json);
1402
+ } else if (opts.name || opts.description || opts.policyId !== void 0 || opts.active || opts.inactive) {
1403
+ const payload = {};
1404
+ if (opts.name) payload.name = opts.name;
1405
+ if (opts.description) payload.description = opts.description;
1406
+ if (opts.policyId !== void 0) payload.policyId = opts.policyId === "null" ? null : opts.policyId;
1407
+ if (opts.active) payload.isActive = true;
1408
+ if (opts.inactive) payload.isActive = false;
1409
+ body = payload;
1410
+ } else {
1411
+ body = await parseJsonInput();
1412
+ }
1413
+ const client = createClient(cmd);
1414
+ const format = getFormat(cmd);
1415
+ const response = await client.rawRequest(
1416
+ "PATCH",
1417
+ `/me/oauth-clients/${encodeURIComponent(String(clientId))}`,
1418
+ { body }
1419
+ );
1420
+ outputResponse(response, format);
1421
+ printSuccess("OAuth client updated.");
1422
+ })
1423
+ );
1424
+ addExamples(update, [
1425
+ {
1426
+ description: "Rename an OAuth client",
1427
+ command: "geonic me oauth-clients update <client-id> --name new-name"
1428
+ },
1429
+ {
1430
+ description: "Attach a policy",
1431
+ command: "geonic me oauth-clients update <client-id> --policy-id <policy-id>"
1432
+ },
1433
+ {
1434
+ description: "Unbind policy",
1435
+ command: "geonic me oauth-clients update <client-id> --policy-id null"
1436
+ },
1437
+ {
1438
+ description: "Deactivate an OAuth client",
1439
+ command: "geonic me oauth-clients update <client-id> --inactive"
1392
1440
  }
1393
1441
  ]);
1394
1442
  const del = oauthClients.command("delete <id>").description("Delete an OAuth client").action(
@@ -1407,6 +1455,25 @@ function addMeOAuthClientsSubcommand(me) {
1407
1455
  command: "geonic me oauth-clients delete <client-id>"
1408
1456
  }
1409
1457
  ]);
1458
+ const regenerateSecret = oauthClients.command("regenerate-secret <clientId>").description("Regenerate the client secret of an OAuth client").action(
1459
+ withErrorHandler(async (clientId, _opts, cmd) => {
1460
+ const client = createClient(cmd);
1461
+ const format = getFormat(cmd);
1462
+ const response = await client.rawRequest(
1463
+ "POST",
1464
+ `/me/oauth-clients/${encodeURIComponent(String(clientId))}/regenerate-secret`
1465
+ );
1466
+ printWarning("Save the new clientSecret now \u2014 it will not be shown again.");
1467
+ outputResponse(response, format);
1468
+ printSuccess("OAuth client secret regenerated.");
1469
+ })
1470
+ );
1471
+ addExamples(regenerateSecret, [
1472
+ {
1473
+ description: "Regenerate client secret",
1474
+ command: "geonic me oauth-clients regenerate-secret <client-id>"
1475
+ }
1476
+ ]);
1410
1477
  }
1411
1478
 
1412
1479
  // src/commands/me-api-keys.ts
@@ -1426,7 +1493,7 @@ function addMeApiKeysSubcommand(me) {
1426
1493
  command: "geonic me api-keys list"
1427
1494
  }
1428
1495
  ]);
1429
- 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("--save", "Save the API key to config for automatic use").action(
1496
+ 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(
1430
1497
  withErrorHandler(async (json, _opts, cmd) => {
1431
1498
  const opts = cmd.opts();
1432
1499
  if (opts.origins !== void 0) {
@@ -1439,12 +1506,11 @@ function addMeApiKeysSubcommand(me) {
1439
1506
  let body;
1440
1507
  if (json) {
1441
1508
  body = await parseJsonInput(json);
1442
- } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0) {
1509
+ } else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0) {
1443
1510
  const payload = {};
1444
1511
  if (opts.name) payload.name = opts.name;
1445
- if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
1512
+ if (opts.policy) payload.policyId = opts.policy;
1446
1513
  if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
1447
- if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
1448
1514
  if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
1449
1515
  if (opts.rateLimit) {
1450
1516
  const raw = opts.rateLimit.trim();
@@ -1494,11 +1560,14 @@ function addMeApiKeysSubcommand(me) {
1494
1560
  console.error("API key created.");
1495
1561
  })
1496
1562
  );
1497
- addNotes(create, API_KEY_SCOPES_HELP_NOTES);
1563
+ addNotes(create, [
1564
+ "Use --policy to attach an existing XACML policy to the API key.",
1565
+ "Manage policies with `geonic admin policies` commands."
1566
+ ]);
1498
1567
  addExamples(create, [
1499
1568
  {
1500
- description: "Create an API key with flags",
1501
- command: "geonic me api-keys create --name my-app --scopes read:entities --origins 'https://example.com'"
1569
+ description: "Create an API key with a policy",
1570
+ command: "geonic me api-keys create --name my-app --policy <policy-id>"
1502
1571
  },
1503
1572
  {
1504
1573
  description: "Create and save API key to config",
@@ -1506,7 +1575,7 @@ function addMeApiKeysSubcommand(me) {
1506
1575
  },
1507
1576
  {
1508
1577
  description: "Create an API key from JSON",
1509
- command: `geonic me api-keys create '{"name":"my-app","allowedScopes":["read:entities"]}'`
1578
+ command: `geonic me api-keys create '{"name":"my-app","policyId":"<policy-id>"}'`
1510
1579
  },
1511
1580
  {
1512
1581
  description: "Create an API key with rate limiting",
@@ -1517,6 +1586,77 @@ function addMeApiKeysSubcommand(me) {
1517
1586
  command: "geonic me api-keys create --name my-app --dpop-required"
1518
1587
  }
1519
1588
  ]);
1589
+ 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(
1590
+ withErrorHandler(async (keyId, json, _opts, cmd) => {
1591
+ const opts = cmd.opts();
1592
+ if (opts.origins !== void 0) {
1593
+ const parsed = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
1594
+ if (parsed.length === 0) {
1595
+ printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
1596
+ process.exit(1);
1597
+ }
1598
+ }
1599
+ let body;
1600
+ if (json) {
1601
+ body = await parseJsonInput(json);
1602
+ } else if (opts.name || opts.policyId !== void 0 || opts.origins !== void 0 || opts.rateLimit || opts.dpopRequired !== void 0 || opts.active || opts.inactive) {
1603
+ const payload = {};
1604
+ if (opts.name) payload.name = opts.name;
1605
+ if (opts.policyId !== void 0) payload.policyId = opts.policyId === "null" ? null : opts.policyId;
1606
+ if (opts.origins !== void 0) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
1607
+ if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
1608
+ if (opts.rateLimit) {
1609
+ const raw = opts.rateLimit.trim();
1610
+ if (!/^\d+$/.test(raw)) {
1611
+ printError("--rate-limit must be a positive integer.");
1612
+ process.exit(1);
1613
+ }
1614
+ const perMinute = Number(raw);
1615
+ if (perMinute <= 0) {
1616
+ printError("--rate-limit must be a positive integer.");
1617
+ process.exit(1);
1618
+ }
1619
+ payload.rateLimit = { perMinute };
1620
+ }
1621
+ if (opts.active) payload.isActive = true;
1622
+ if (opts.inactive) payload.isActive = false;
1623
+ body = payload;
1624
+ } else {
1625
+ body = await parseJsonInput();
1626
+ }
1627
+ const client = createClient(cmd);
1628
+ const format = getFormat(cmd);
1629
+ const response = await client.rawRequest(
1630
+ "PATCH",
1631
+ `/me/api-keys/${encodeURIComponent(String(keyId))}`,
1632
+ { body }
1633
+ );
1634
+ outputResponse(response, format);
1635
+ console.error("API key updated.");
1636
+ })
1637
+ );
1638
+ addExamples(update, [
1639
+ {
1640
+ description: "Rename an API key",
1641
+ command: "geonic me api-keys update <key-id> --name new-name"
1642
+ },
1643
+ {
1644
+ description: "Attach a policy",
1645
+ command: "geonic me api-keys update <key-id> --policy-id <policy-id>"
1646
+ },
1647
+ {
1648
+ description: "Unbind policy",
1649
+ command: "geonic me api-keys update <key-id> --policy-id null"
1650
+ },
1651
+ {
1652
+ description: "Deactivate an API key",
1653
+ command: "geonic me api-keys update <key-id> --inactive"
1654
+ },
1655
+ {
1656
+ description: "Update from JSON",
1657
+ command: `geonic me api-keys update <key-id> '{"name":"new-name","rateLimit":{"perMinute":60}}'`
1658
+ }
1659
+ ]);
1520
1660
  const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
1521
1661
  withErrorHandler(async (keyId, _opts, cmd) => {
1522
1662
  const client = createClient(cmd);
@@ -1535,6 +1675,149 @@ function addMeApiKeysSubcommand(me) {
1535
1675
  ]);
1536
1676
  }
1537
1677
 
1678
+ // src/commands/me-policies.ts
1679
+ function addMePoliciesSubcommand(me) {
1680
+ const policies = me.command("policies").description("Manage your personal XACML policies");
1681
+ const list = policies.command("list").description("List your personal policies").action(
1682
+ withErrorHandler(async (_opts, cmd) => {
1683
+ const client = createClient(cmd);
1684
+ const format = getFormat(cmd);
1685
+ const response = await client.rawRequest("GET", "/me/policies");
1686
+ outputResponse(response, format);
1687
+ })
1688
+ );
1689
+ addExamples(list, [
1690
+ {
1691
+ description: "List your personal policies",
1692
+ command: "geonic me policies list"
1693
+ }
1694
+ ]);
1695
+ const get = policies.command("get <policyId>").description("Get a personal policy by ID").action(
1696
+ withErrorHandler(async (policyId, _opts, cmd) => {
1697
+ const client = createClient(cmd);
1698
+ const format = getFormat(cmd);
1699
+ const response = await client.rawRequest(
1700
+ "GET",
1701
+ `/me/policies/${encodeURIComponent(String(policyId))}`
1702
+ );
1703
+ outputResponse(response, format);
1704
+ })
1705
+ );
1706
+ addExamples(get, [
1707
+ {
1708
+ description: "Get a personal policy by ID",
1709
+ command: "geonic me policies get <policy-id>"
1710
+ }
1711
+ ]);
1712
+ const create = policies.command("create [json]").description(
1713
+ `Create a personal XACML policy
1714
+
1715
+ Constraints (enforced server-side):
1716
+ - priority is fixed at 100 (user role minimum)
1717
+ - scope is 'personal' \u2014 not applied tenant-wide
1718
+ - target is required
1719
+ - data API paths only (/v2/**, /ngsi-ld/** etc.)
1720
+
1721
+ Example \u2014 GET-only policy for /v2/**:
1722
+ {
1723
+ "policyId": "my-readonly",
1724
+ "target": {
1725
+ "resources": [{"attributeId": "path", "matchValue": "/v2/**", "matchFunction": "glob"}]
1726
+ },
1727
+ "rules": [
1728
+ {"ruleId": "allow-get", "effect": "Permit", "target": {"actions": [{"attributeId": "method", "matchValue": "GET"}]}},
1729
+ {"ruleId": "deny-others", "effect": "Deny"}
1730
+ ]
1731
+ }`
1732
+ ).option("--policy-id <id>", "Policy ID (auto-generated UUID if omitted)").option("--description <text>", "Policy description").action(
1733
+ withErrorHandler(async (json, _opts, cmd) => {
1734
+ const opts = cmd.opts();
1735
+ let body;
1736
+ if (json) {
1737
+ body = await parseJsonInput(json);
1738
+ } else if (opts.policyId || opts.description) {
1739
+ const payload = {};
1740
+ if (opts.policyId) payload.policyId = opts.policyId;
1741
+ if (opts.description) payload.description = opts.description;
1742
+ body = payload;
1743
+ } else {
1744
+ body = await parseJsonInput();
1745
+ }
1746
+ const client = createClient(cmd);
1747
+ const format = getFormat(cmd);
1748
+ const response = await client.rawRequest("POST", "/me/policies", { body });
1749
+ outputResponse(response, format);
1750
+ printSuccess("Policy created.");
1751
+ })
1752
+ );
1753
+ addNotes(create, [
1754
+ "priority is always set to 100 by the server regardless of the value you specify.",
1755
+ "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`."
1756
+ ]);
1757
+ addExamples(create, [
1758
+ {
1759
+ description: "Create a GET-only policy from inline JSON",
1760
+ 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"}]}'`
1761
+ },
1762
+ {
1763
+ description: "Create from a JSON file",
1764
+ command: "geonic me policies create @policy.json"
1765
+ },
1766
+ {
1767
+ description: "Create from stdin",
1768
+ command: "cat policy.json | geonic me policies create"
1769
+ }
1770
+ ]);
1771
+ const update = policies.command("update <policyId> [json]").description("Update a personal policy (partial update)").option("--description <text>", "Policy description").action(
1772
+ withErrorHandler(async (policyId, json, _opts, cmd) => {
1773
+ const opts = cmd.opts();
1774
+ let body;
1775
+ if (json) {
1776
+ body = await parseJsonInput(json);
1777
+ } else if (opts.description) {
1778
+ body = { description: opts.description };
1779
+ } else {
1780
+ body = await parseJsonInput();
1781
+ }
1782
+ const client = createClient(cmd);
1783
+ const format = getFormat(cmd);
1784
+ const response = await client.rawRequest(
1785
+ "PATCH",
1786
+ `/me/policies/${encodeURIComponent(String(policyId))}`,
1787
+ { body }
1788
+ );
1789
+ outputResponse(response, format);
1790
+ printSuccess("Policy updated.");
1791
+ })
1792
+ );
1793
+ addExamples(update, [
1794
+ {
1795
+ description: "Update policy rules",
1796
+ command: `geonic me policies update <policy-id> '{"rules":[{"ruleId":"allow-get","effect":"Permit"}]}'`
1797
+ },
1798
+ {
1799
+ description: "Update from a JSON file",
1800
+ command: "geonic me policies update <policy-id> @patch.json"
1801
+ }
1802
+ ]);
1803
+ const del = policies.command("delete <policyId>").description("Delete a personal policy").action(
1804
+ withErrorHandler(async (policyId, _opts, cmd) => {
1805
+ const client = createClient(cmd);
1806
+ await client.rawRequest(
1807
+ "DELETE",
1808
+ `/me/policies/${encodeURIComponent(String(policyId))}`
1809
+ );
1810
+ printSuccess("Policy deleted.");
1811
+ })
1812
+ );
1813
+ addExamples(del, [
1814
+ {
1815
+ description: "Delete a personal policy",
1816
+ command: "geonic me policies delete <policy-id>"
1817
+ }
1818
+ ]);
1819
+ }
1820
+
1538
1821
  // src/commands/auth.ts
1539
1822
  function createLoginCommand() {
1540
1823
  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(
@@ -1866,6 +2149,7 @@ function registerAuthCommands(program2) {
1866
2149
  ]);
1867
2150
  addMeOAuthClientsSubcommand(me);
1868
2151
  addMeApiKeysSubcommand(me);
2152
+ addMePoliciesSubcommand(me);
1869
2153
  program2.addCommand(createLoginCommand(), { hidden: true });
1870
2154
  program2.addCommand(createLogoutCommand(), { hidden: true });
1871
2155
  const hiddenWhoami = new Command("whoami").description("Display current authenticated user").action(createMeAction());
@@ -3201,7 +3485,7 @@ function registerUsersCommand(parent) {
3201
3485
  }
3202
3486
  ]);
3203
3487
  const create = users.command("create [json]").description(
3204
- 'Create a new user\n\nJSON payload example:\n {\n "email": "user@example.com",\n "password": "SecurePassword123!",\n "role": "super_admin"\n }'
3488
+ 'Create a new user\n\nJSON payload example:\n {\n "email": "user@example.com",\n "password": "SecurePassword123!",\n "role": "tenant_admin",\n "tenantId": "<tenant-id>"\n }\n\nRoles: super_admin, tenant_admin, user\ntenantId is required for tenant_admin and user roles.'
3205
3489
  ).action(
3206
3490
  withErrorHandler(async (json, _opts, cmd) => {
3207
3491
  const body = await parseJsonInput(json);
@@ -3216,8 +3500,12 @@ function registerUsersCommand(parent) {
3216
3500
  );
3217
3501
  addExamples(create, [
3218
3502
  {
3219
- description: "Create with inline JSON",
3220
- command: `geonic admin users create '{"email":"user@example.com","password":"SecurePassword123!","role":"super_admin"}'`
3503
+ description: "Create a tenant admin",
3504
+ command: `geonic admin users create '{"email":"admin@example.com","password":"SecurePass12345!","role":"tenant_admin","tenantId":"<tenant-id>"}'`
3505
+ },
3506
+ {
3507
+ description: "Create a user for a tenant",
3508
+ command: `geonic admin users create '{"email":"user@example.com","password":"SecurePass12345!","role":"user","tenantId":"<tenant-id>"}'`
3221
3509
  },
3222
3510
  {
3223
3511
  description: "Create from a JSON file",
@@ -3361,7 +3649,7 @@ function registerPoliciesCommand(parent) {
3361
3649
  }
3362
3650
  ]);
3363
3651
  const create = policies.command("create [json]").description(
3364
- 'Create a new policy\n\nJSON payload example:\n {\n "description": "Allow all entities",\n "rules": [{"ruleId": "allow-all", "effect": "Permit"}]\n }'
3652
+ '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'
3365
3653
  ).action(
3366
3654
  withErrorHandler(async (json, _opts, cmd) => {
3367
3655
  const body = await parseJsonInput(json);
@@ -3379,6 +3667,18 @@ function registerPoliciesCommand(parent) {
3379
3667
  description: "Create with inline JSON",
3380
3668
  command: `geonic admin policies create '{"description":"Allow all entities","rules":[{"ruleId":"allow-all","effect":"Permit"}]}'`
3381
3669
  },
3670
+ {
3671
+ description: "Create with target (entity type + method)",
3672
+ command: `geonic admin policies create '{"description":"Allow GET Landmark","target":{"resources":[{"attributeId":"entityType","matchValue":"Landmark"}],"actions":[{"attributeId":"method","matchValue":"GET"}]},"rules":[{"ruleId":"permit-get","effect":"Permit"}]}'`
3673
+ },
3674
+ {
3675
+ description: "Create anonymous access policy",
3676
+ command: `geonic admin policies create '{"policyId":"public-read","target":{"subjects":[{"attributeId":"role","matchValue":"anonymous"}],"resources":[{"attributeId":"entityType","matchValue":"WeatherObserved"}],"actions":[{"attributeId":"method","matchValue":"GET"}]},"rules":[{"effect":"Permit"}]}'`
3677
+ },
3678
+ {
3679
+ description: "Create servicePath-based policy (glob match)",
3680
+ command: `geonic admin policies create '{"description":"Allow read on /opendata/**","priority":100,"target":{"resources":[{"attributeId":"servicePath","matchValue":"/opendata/**","matchFunction":"glob"}],"actions":[{"attributeId":"method","matchValue":"GET"}]},"rules":[{"effect":"Permit"}]}'`
3681
+ },
3382
3682
  {
3383
3683
  description: "Create from a JSON file",
3384
3684
  command: "geonic admin policies create @policy.json"
@@ -3505,7 +3805,7 @@ function registerOAuthClientsCommand(parent) {
3505
3805
  }
3506
3806
  ]);
3507
3807
  const create = oauthClients.command("create [json]").description(
3508
- 'Create a new OAuth client\n\nJSON payload example:\n {\n "clientName": "my-app",\n "allowedScopes": ["read:entities", "write:entities"]\n }'
3808
+ 'Create a new OAuth client\n\nJSON payload example:\n {\n "name": "my-app",\n "policyId": "<policy-id>"\n }'
3509
3809
  ).action(
3510
3810
  withErrorHandler(async (json, _opts, cmd) => {
3511
3811
  const body = await parseJsonInput(json);
@@ -3521,7 +3821,7 @@ function registerOAuthClientsCommand(parent) {
3521
3821
  addExamples(create, [
3522
3822
  {
3523
3823
  description: "Create with inline JSON",
3524
- command: `geonic admin oauth-clients create '{"clientName":"my-app","allowedScopes":["read:entities","write:entities"]}'`
3824
+ command: `geonic admin oauth-clients create '{"name":"my-app","policyId":"<policy-id>"}'`
3525
3825
  },
3526
3826
  {
3527
3827
  description: "Create from a JSON file",
@@ -3660,9 +3960,8 @@ function validateOrigins(body, opts) {
3660
3960
  function buildBodyFromFlags(opts) {
3661
3961
  const payload = {};
3662
3962
  if (opts.name) payload.name = opts.name;
3663
- if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
3963
+ if (opts.policy) payload.policyId = opts.policy;
3664
3964
  if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
3665
- if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
3666
3965
  if (opts.rateLimit) {
3667
3966
  const raw = String(opts.rateLimit).trim();
3668
3967
  if (!/^\d+$/.test(raw)) {
@@ -3722,14 +4021,14 @@ function registerApiKeysCommand(parent) {
3722
4021
  command: "geonic admin api-keys get <key-id>"
3723
4022
  }
3724
4023
  ]);
3725
- 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("--tenant-id <id>", "Tenant ID").option("--save", "Save the API key to profile config").action(
4024
+ 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(
3726
4025
  withErrorHandler(async (json, _opts, cmd) => {
3727
4026
  const opts = cmd.opts();
3728
4027
  validateOrigins(void 0, opts);
3729
4028
  let body;
3730
4029
  if (json) {
3731
4030
  body = await parseJsonInput(json);
3732
- } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.tenantId) {
4031
+ } else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0 || opts.tenantId) {
3733
4032
  body = buildBodyFromFlags(opts);
3734
4033
  } else {
3735
4034
  body = await parseJsonInput();
@@ -3761,11 +4060,14 @@ function registerApiKeysCommand(parent) {
3761
4060
  console.error("API key created.");
3762
4061
  })
3763
4062
  );
3764
- addNotes(create, API_KEY_SCOPES_HELP_NOTES);
4063
+ addNotes(create, [
4064
+ "Use --policy to attach an existing XACML policy to the API key.",
4065
+ "Manage policies with `geonic admin policies` commands."
4066
+ ]);
3765
4067
  addExamples(create, [
3766
4068
  {
3767
- description: "Create an API key with flags",
3768
- command: "geonic admin api-keys create --name my-key --scopes read:entities,write:entities --origins '*'"
4069
+ description: "Create an API key with a policy",
4070
+ command: "geonic admin api-keys create --name my-key --policy <policy-id> --origins '*'"
3769
4071
  },
3770
4072
  {
3771
4073
  description: "Create an API key with DPoP required",
@@ -3776,7 +4078,7 @@ function registerApiKeysCommand(parent) {
3776
4078
  command: "geonic admin api-keys create @key.json --save"
3777
4079
  }
3778
4080
  ]);
3779
- 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").action(
4081
+ 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(
3780
4082
  withErrorHandler(
3781
4083
  async (keyId, json, _opts, cmd) => {
3782
4084
  const opts = cmd.opts();
@@ -3784,7 +4086,7 @@ function registerApiKeysCommand(parent) {
3784
4086
  let body;
3785
4087
  if (json) {
3786
4088
  body = await parseJsonInput(json);
3787
- } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0) {
4089
+ } else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0) {
3788
4090
  body = buildBodyFromFlags(opts);
3789
4091
  } else {
3790
4092
  body = await parseJsonInput();
@@ -3802,12 +4104,19 @@ function registerApiKeysCommand(parent) {
3802
4104
  }
3803
4105
  )
3804
4106
  );
3805
- addNotes(update, API_KEY_SCOPES_HELP_NOTES);
4107
+ addNotes(update, [
4108
+ "Use --policy to attach an existing XACML policy to the API key.",
4109
+ "Manage policies with `geonic admin policies` commands."
4110
+ ]);
3806
4111
  addExamples(update, [
3807
4112
  {
3808
4113
  description: "Update an API key name",
3809
4114
  command: "geonic admin api-keys update <key-id> --name new-name"
3810
4115
  },
4116
+ {
4117
+ description: "Attach a policy",
4118
+ command: "geonic admin api-keys update <key-id> --policy <policy-id>"
4119
+ },
3811
4120
  {
3812
4121
  description: "Enable DPoP requirement",
3813
4122
  command: "geonic admin api-keys update <key-id> --dpop-required"