@geolonia/geonicdb-cli 0.7.0 → 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,31 +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
- "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
949
  function resolveOptions(cmd) {
966
950
  const opts = cmd.optsWithGlobals();
967
951
  const config = loadConfig(opts.profile);
@@ -1027,6 +1011,8 @@ function withErrorHandler(fn) {
1027
1011
  }
1028
1012
  if (err instanceof GdbClientError && err.status === 401) {
1029
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).");
1030
1016
  } else if (err instanceof GdbClientError && err.status === 403) {
1031
1017
  const detail = (err.ngsiError?.detail ?? err.ngsiError?.description ?? "").toLowerCase();
1032
1018
  if (detail.includes("entity type") || detail.includes("allowedentitytypes")) {
@@ -1335,16 +1321,16 @@ function addMeOAuthClientsSubcommand(me) {
1335
1321
  command: "geonic me oauth-clients list"
1336
1322
  }
1337
1323
  ]);
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(
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(
1339
1325
  withErrorHandler(async (json, _opts, cmd) => {
1340
1326
  const opts = cmd.opts();
1341
1327
  let body;
1342
1328
  if (json) {
1343
1329
  body = await parseJsonInput(json);
1344
- } else if (opts.name || opts.scopes) {
1330
+ } else if (opts.name || opts.policy) {
1345
1331
  const payload = {};
1346
- if (opts.name) payload.clientName = opts.name;
1347
- 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;
1348
1334
  body = payload;
1349
1335
  } else {
1350
1336
  body = await parseJsonInput();
@@ -1367,8 +1353,7 @@ function addMeOAuthClientsSubcommand(me) {
1367
1353
  const tokenResult = await clientCredentialsGrant({
1368
1354
  baseUrl,
1369
1355
  clientId,
1370
- clientSecret,
1371
- scope: data.allowedScopes?.join(" ")
1356
+ clientSecret
1372
1357
  });
1373
1358
  const config = loadConfig(globalOpts.profile);
1374
1359
  config.clientId = clientId;
@@ -1386,11 +1371,18 @@ function addMeOAuthClientsSubcommand(me) {
1386
1371
  printSuccess("OAuth client created.");
1387
1372
  })
1388
1373
  );
1389
- 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
+ ]);
1390
1378
  addExamples(create, [
1391
1379
  {
1392
1380
  description: "Create an OAuth client with flags",
1393
- 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>"
1394
1386
  },
1395
1387
  {
1396
1388
  description: "Create and save credentials for auto-reauth",
@@ -1398,7 +1390,53 @@ function addMeOAuthClientsSubcommand(me) {
1398
1390
  },
1399
1391
  {
1400
1392
  description: "Create an OAuth client from JSON",
1401
- 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"
1402
1440
  }
1403
1441
  ]);
1404
1442
  const del = oauthClients.command("delete <id>").description("Delete an OAuth client").action(
@@ -1417,6 +1455,25 @@ function addMeOAuthClientsSubcommand(me) {
1417
1455
  command: "geonic me oauth-clients delete <client-id>"
1418
1456
  }
1419
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
+ ]);
1420
1477
  }
1421
1478
 
1422
1479
  // src/commands/me-api-keys.ts
@@ -1436,7 +1493,7 @@ function addMeApiKeysSubcommand(me) {
1436
1493
  command: "geonic me api-keys list"
1437
1494
  }
1438
1495
  ]);
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(
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(
1440
1497
  withErrorHandler(async (json, _opts, cmd) => {
1441
1498
  const opts = cmd.opts();
1442
1499
  if (opts.origins !== void 0) {
@@ -1449,14 +1506,12 @@ function addMeApiKeysSubcommand(me) {
1449
1506
  let body;
1450
1507
  if (json) {
1451
1508
  body = await parseJsonInput(json);
1452
- } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.permissions) {
1509
+ } else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0) {
1453
1510
  const payload = {};
1454
1511
  if (opts.name) payload.name = opts.name;
1455
- if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
1512
+ if (opts.policy) payload.policyId = opts.policy;
1456
1513
  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
1514
  if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
1459
- if (opts.permissions) payload.permissions = parsePermissions(opts.permissions);
1460
1515
  if (opts.rateLimit) {
1461
1516
  const raw = opts.rateLimit.trim();
1462
1517
  if (!/^\d+$/.test(raw)) {
@@ -1506,20 +1561,13 @@ function addMeApiKeysSubcommand(me) {
1506
1561
  })
1507
1562
  );
1508
1563
  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)."
1564
+ "Use --policy to attach an existing XACML policy to the API key.",
1565
+ "Manage policies with `geonic admin policies` commands."
1514
1566
  ]);
1515
1567
  addExamples(create, [
1516
1568
  {
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"
1569
+ description: "Create an API key with a policy",
1570
+ command: "geonic me api-keys create --name my-app --policy <policy-id>"
1523
1571
  },
1524
1572
  {
1525
1573
  description: "Create and save API key to config",
@@ -1527,7 +1575,7 @@ function addMeApiKeysSubcommand(me) {
1527
1575
  },
1528
1576
  {
1529
1577
  description: "Create an API key from JSON",
1530
- 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>"}'`
1531
1579
  },
1532
1580
  {
1533
1581
  description: "Create an API key with rate limiting",
@@ -1538,6 +1586,77 @@ function addMeApiKeysSubcommand(me) {
1538
1586
  command: "geonic me api-keys create --name my-app --dpop-required"
1539
1587
  }
1540
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
+ ]);
1541
1660
  const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
1542
1661
  withErrorHandler(async (keyId, _opts, cmd) => {
1543
1662
  const client = createClient(cmd);
@@ -1556,6 +1675,149 @@ function addMeApiKeysSubcommand(me) {
1556
1675
  ]);
1557
1676
  }
1558
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
+
1559
1821
  // src/commands/auth.ts
1560
1822
  function createLoginCommand() {
1561
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(
@@ -1887,6 +2149,7 @@ function registerAuthCommands(program2) {
1887
2149
  ]);
1888
2150
  addMeOAuthClientsSubcommand(me);
1889
2151
  addMeApiKeysSubcommand(me);
2152
+ addMePoliciesSubcommand(me);
1890
2153
  program2.addCommand(createLoginCommand(), { hidden: true });
1891
2154
  program2.addCommand(createLogoutCommand(), { hidden: true });
1892
2155
  const hiddenWhoami = new Command("whoami").description("Display current authenticated user").action(createMeAction());
@@ -3386,7 +3649,7 @@ function registerPoliciesCommand(parent) {
3386
3649
  }
3387
3650
  ]);
3388
3651
  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'
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'
3390
3653
  ).action(
3391
3654
  withErrorHandler(async (json, _opts, cmd) => {
3392
3655
  const body = await parseJsonInput(json);
@@ -3542,7 +3805,7 @@ function registerOAuthClientsCommand(parent) {
3542
3805
  }
3543
3806
  ]);
3544
3807
  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 }'
3808
+ 'Create a new OAuth client\n\nJSON payload example:\n {\n "name": "my-app",\n "policyId": "<policy-id>"\n }'
3546
3809
  ).action(
3547
3810
  withErrorHandler(async (json, _opts, cmd) => {
3548
3811
  const body = await parseJsonInput(json);
@@ -3558,7 +3821,7 @@ function registerOAuthClientsCommand(parent) {
3558
3821
  addExamples(create, [
3559
3822
  {
3560
3823
  description: "Create with inline JSON",
3561
- 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>"}'`
3562
3825
  },
3563
3826
  {
3564
3827
  description: "Create from a JSON file",
@@ -3697,9 +3960,8 @@ function validateOrigins(body, opts) {
3697
3960
  function buildBodyFromFlags(opts) {
3698
3961
  const payload = {};
3699
3962
  if (opts.name) payload.name = opts.name;
3700
- if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
3963
+ if (opts.policy) payload.policyId = opts.policy;
3701
3964
  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
3965
  if (opts.rateLimit) {
3704
3966
  const raw = String(opts.rateLimit).trim();
3705
3967
  if (!/^\d+$/.test(raw)) {
@@ -3714,7 +3976,6 @@ function buildBodyFromFlags(opts) {
3714
3976
  payload.rateLimit = { perMinute };
3715
3977
  }
3716
3978
  if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
3717
- if (opts.permissions) payload.permissions = parsePermissions(opts.permissions);
3718
3979
  if (opts.tenantId) payload.tenantId = opts.tenantId;
3719
3980
  return payload;
3720
3981
  }
@@ -3760,14 +4021,14 @@ function registerApiKeysCommand(parent) {
3760
4021
  command: "geonic admin api-keys get <key-id>"
3761
4022
  }
3762
4023
  ]);
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(
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(
3764
4025
  withErrorHandler(async (json, _opts, cmd) => {
3765
4026
  const opts = cmd.opts();
3766
4027
  validateOrigins(void 0, opts);
3767
4028
  let body;
3768
4029
  if (json) {
3769
4030
  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) {
4031
+ } else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0 || opts.tenantId) {
3771
4032
  body = buildBodyFromFlags(opts);
3772
4033
  } else {
3773
4034
  body = await parseJsonInput();
@@ -3800,20 +4061,13 @@ function registerApiKeysCommand(parent) {
3800
4061
  })
3801
4062
  );
3802
4063
  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)."
4064
+ "Use --policy to attach an existing XACML policy to the API key.",
4065
+ "Manage policies with `geonic admin policies` commands."
3808
4066
  ]);
3809
4067
  addExamples(create, [
3810
4068
  {
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 '*'"
4069
+ description: "Create an API key with a policy",
4070
+ command: "geonic admin api-keys create --name my-key --policy <policy-id> --origins '*'"
3817
4071
  },
3818
4072
  {
3819
4073
  description: "Create an API key with DPoP required",
@@ -3824,7 +4078,7 @@ function registerApiKeysCommand(parent) {
3824
4078
  command: "geonic admin api-keys create @key.json --save"
3825
4079
  }
3826
4080
  ]);
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(
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(
3828
4082
  withErrorHandler(
3829
4083
  async (keyId, json, _opts, cmd) => {
3830
4084
  const opts = cmd.opts();
@@ -3832,7 +4086,7 @@ function registerApiKeysCommand(parent) {
3832
4086
  let body;
3833
4087
  if (json) {
3834
4088
  body = await parseJsonInput(json);
3835
- } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.permissions) {
4089
+ } else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0) {
3836
4090
  body = buildBodyFromFlags(opts);
3837
4091
  } else {
3838
4092
  body = await parseJsonInput();
@@ -3851,11 +4105,8 @@ function registerApiKeysCommand(parent) {
3851
4105
  )
3852
4106
  );
3853
4107
  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)."
4108
+ "Use --policy to attach an existing XACML policy to the API key.",
4109
+ "Manage policies with `geonic admin policies` commands."
3859
4110
  ]);
3860
4111
  addExamples(update, [
3861
4112
  {
@@ -3863,8 +4114,8 @@ function registerApiKeysCommand(parent) {
3863
4114
  command: "geonic admin api-keys update <key-id> --name new-name"
3864
4115
  },
3865
4116
  {
3866
- description: "Update permissions",
3867
- command: "geonic admin api-keys update <key-id> --permissions read,write"
4117
+ description: "Attach a policy",
4118
+ command: "geonic admin api-keys update <key-id> --policy <policy-id>"
3868
4119
  },
3869
4120
  {
3870
4121
  description: "Enable DPoP requirement",