@geolonia/geonicdb-cli 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -1
- package/dist/index.js +468 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,10 +118,28 @@ geonic help [<command>] [<subcommand>]
|
|
|
118
118
|
|---|---|
|
|
119
119
|
| `auth login` | Authenticate and save token |
|
|
120
120
|
| `auth logout` | Clear saved authentication token |
|
|
121
|
+
| `auth nonce` | Get a nonce and PoW challenge for API key authentication |
|
|
122
|
+
| `auth token-exchange` | Exchange API key for a session JWT via nonce + PoW |
|
|
121
123
|
|
|
122
124
|
The `auth login` command reads `GDB_EMAIL` and `GDB_PASSWORD` environment variables. It also supports OAuth Client Credentials flow with `--client-id` and `--client-secret`.
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
#### API Key Token Exchange
|
|
127
|
+
|
|
128
|
+
`auth token-exchange` performs a complete API key to JWT exchange:
|
|
129
|
+
|
|
130
|
+
1. Requests a nonce from the server (`POST /auth/nonce`)
|
|
131
|
+
2. Solves the Proof-of-Work challenge (SHA-256)
|
|
132
|
+
3. Exchanges the API key + solved PoW for a session JWT (`POST /oauth/token`)
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Exchange API key for JWT and save to config
|
|
136
|
+
geonic auth token-exchange --api-key gdb_abcdef... --save
|
|
137
|
+
|
|
138
|
+
# Just display the token without saving
|
|
139
|
+
geonic auth token-exchange --api-key gdb_abcdef...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### me — Current user and self-service resources
|
|
125
143
|
|
|
126
144
|
```bash
|
|
127
145
|
geonic me
|
|
@@ -129,6 +147,44 @@ geonic me
|
|
|
129
147
|
|
|
130
148
|
Displays the current authenticated user, token expiry, and active profile.
|
|
131
149
|
|
|
150
|
+
#### me oauth-clients
|
|
151
|
+
|
|
152
|
+
| Subcommand | Description |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `me oauth-clients list` | List your OAuth clients |
|
|
155
|
+
| `me oauth-clients create [json]` | Create a new OAuth client |
|
|
156
|
+
| `me oauth-clients delete <id>` | Delete an OAuth client |
|
|
157
|
+
|
|
158
|
+
#### me api-keys
|
|
159
|
+
|
|
160
|
+
| Subcommand | Description |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `me api-keys list` | List your API keys |
|
|
163
|
+
| `me api-keys create [json]` | Create a new API key |
|
|
164
|
+
| `me api-keys delete <keyId>` | Delete an API key |
|
|
165
|
+
|
|
166
|
+
`me api-keys create` supports flag options:
|
|
167
|
+
|
|
168
|
+
| Flag | Description |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `--name <name>` | Key name |
|
|
171
|
+
| `--scopes <scopes>` | Allowed scopes (comma-separated) |
|
|
172
|
+
| `--origins <origins>` | Allowed origins (comma-separated, at least 1 required) |
|
|
173
|
+
| `--entity-types <types>` | Allowed entity types (comma-separated) |
|
|
174
|
+
| `--rate-limit <n>` | Rate limit (requests per minute) |
|
|
175
|
+
| `--dpop-required` | Require DPoP token binding (RFC 9449) |
|
|
176
|
+
| `--save` | Save the API key to profile config |
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Create an API key and save to config
|
|
180
|
+
geonic me api-keys create --name my-app --scopes read:entities --save
|
|
181
|
+
|
|
182
|
+
# Create from JSON
|
|
183
|
+
geonic me api-keys create '{"name":"my-app","allowedScopes":["read:entities"]}'
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`me api-keys list` output includes a `dpopRequired` field (boolean).
|
|
187
|
+
|
|
132
188
|
### entities — Manage context entities
|
|
133
189
|
|
|
134
190
|
| Subcommand | Description |
|
|
@@ -306,6 +362,20 @@ Temporal entityOperations query supports: `--aggr-methods`, `--aggr-period`.
|
|
|
306
362
|
| `admin oauth-clients update <id> [json]` | Update an OAuth client |
|
|
307
363
|
| `admin oauth-clients delete <id>` | Delete an OAuth client |
|
|
308
364
|
|
|
365
|
+
#### admin api-keys
|
|
366
|
+
|
|
367
|
+
| Subcommand | Description |
|
|
368
|
+
|---|---|
|
|
369
|
+
| `admin api-keys list` | List all API keys |
|
|
370
|
+
| `admin api-keys get <keyId>` | Get an API key by ID |
|
|
371
|
+
| `admin api-keys create [json]` | Create a new API key |
|
|
372
|
+
| `admin api-keys update <keyId> [json]` | Update an API key |
|
|
373
|
+
| `admin api-keys delete <keyId>` | Delete an API key |
|
|
374
|
+
|
|
375
|
+
`admin api-keys list` supports `--tenant-id` to filter by tenant. `admin api-keys create` supports flag options: `--name`, `--scopes`, `--origins`, `--entity-types`, `--rate-limit`, `--dpop-required`, `--tenant-id`, `--save`. `admin api-keys update` supports `--name`, `--scopes`, `--origins`, `--entity-types`, `--rate-limit`, `--dpop-required` / `--no-dpop-required`.
|
|
376
|
+
|
|
377
|
+
**Note**: `allowedOrigins` must contain at least 1 item when specified. Use `*` to allow all origins. `allowedEntityTypes` is enforced at runtime — API key holders can only access entities of the specified types. `admin api-keys list` / `admin api-keys get` output includes a `dpopRequired` field (boolean).
|
|
378
|
+
|
|
309
379
|
#### admin cadde
|
|
310
380
|
|
|
311
381
|
| Subcommand | Description |
|
|
@@ -435,6 +505,31 @@ Override the config directory with the `GEONIC_CONFIG_DIR` environment variable:
|
|
|
435
505
|
GEONIC_CONFIG_DIR=/path/to/config geonic entities list
|
|
436
506
|
```
|
|
437
507
|
|
|
508
|
+
## API Key Authentication
|
|
509
|
+
|
|
510
|
+
API keys provide an alternative to JWT tokens for authentication. When configured, requests include the `X-Api-Key` header.
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
# Set API key in config
|
|
514
|
+
geonic config set api-key gdb_your_api_key_here
|
|
515
|
+
|
|
516
|
+
# Or pass via CLI flag
|
|
517
|
+
geonic entities list --api-key gdb_your_api_key_here
|
|
518
|
+
|
|
519
|
+
# Or use environment variable
|
|
520
|
+
GDB_API_KEY=gdb_your_api_key_here geonic entities list
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
When both a Bearer token and an API key are configured, both headers are sent (the server determines precedence).
|
|
524
|
+
|
|
525
|
+
### Valid Scopes
|
|
526
|
+
|
|
527
|
+
`read:entities`, `write:entities`, `read:subscriptions`, `write:subscriptions`, `read:registrations`, `write:registrations`
|
|
528
|
+
|
|
529
|
+
### Entity Type Restrictions
|
|
530
|
+
|
|
531
|
+
API keys with `allowedEntityTypes` are restricted to the specified entity types at runtime. Attempting to access entities of other types results in a 403 error with a descriptive message.
|
|
532
|
+
|
|
438
533
|
## Development
|
|
439
534
|
|
|
440
535
|
Requires Node.js >= 20.
|
package/dist/index.js
CHANGED
|
@@ -602,6 +602,7 @@ function registerConfigCommand(program2) {
|
|
|
602
602
|
}
|
|
603
603
|
|
|
604
604
|
// src/commands/auth.ts
|
|
605
|
+
import { createHash } from "crypto";
|
|
605
606
|
import { Command } from "commander";
|
|
606
607
|
|
|
607
608
|
// src/oauth.ts
|
|
@@ -673,7 +674,7 @@ var GdbClient = class _GdbClient {
|
|
|
673
674
|
if (this.token) {
|
|
674
675
|
headers["Authorization"] = `Bearer ${this.token}`;
|
|
675
676
|
} else if (this.apiKey) {
|
|
676
|
-
headers["
|
|
677
|
+
headers["X-Api-Key"] = this.apiKey;
|
|
677
678
|
}
|
|
678
679
|
if (extra) {
|
|
679
680
|
Object.assign(headers, extra);
|
|
@@ -694,13 +695,15 @@ var GdbClient = class _GdbClient {
|
|
|
694
695
|
getBasePath() {
|
|
695
696
|
return "/ngsi-ld/v1";
|
|
696
697
|
}
|
|
697
|
-
static SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization"]);
|
|
698
|
+
static SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "x-api-key"]);
|
|
698
699
|
static SENSITIVE_BODY_KEYS = /* @__PURE__ */ new Set([
|
|
699
700
|
"password",
|
|
700
701
|
"refreshToken",
|
|
701
702
|
"token",
|
|
702
703
|
"client_secret",
|
|
703
|
-
"clientSecret"
|
|
704
|
+
"clientSecret",
|
|
705
|
+
"key",
|
|
706
|
+
"apiKey"
|
|
704
707
|
]);
|
|
705
708
|
logRequest(method, url, headers, body) {
|
|
706
709
|
if (!this.verbose) return;
|
|
@@ -983,6 +986,13 @@ function withErrorHandler(fn) {
|
|
|
983
986
|
}
|
|
984
987
|
if (err instanceof GdbClientError && err.status === 401) {
|
|
985
988
|
printError("Authentication failed. Please run `geonic login` to re-authenticate.");
|
|
989
|
+
} else if (err instanceof GdbClientError && err.status === 403) {
|
|
990
|
+
const detail = (err.ngsiError?.detail ?? err.ngsiError?.description ?? "").toLowerCase();
|
|
991
|
+
if (detail.includes("entity type") || detail.includes("allowedentitytypes")) {
|
|
992
|
+
printError(`Entity type restriction: ${err.message}`);
|
|
993
|
+
} else {
|
|
994
|
+
printError(err.message);
|
|
995
|
+
}
|
|
986
996
|
} else if (err instanceof Error) {
|
|
987
997
|
printError(err.message);
|
|
988
998
|
} else {
|
|
@@ -1343,6 +1353,131 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1343
1353
|
]);
|
|
1344
1354
|
}
|
|
1345
1355
|
|
|
1356
|
+
// src/commands/me-api-keys.ts
|
|
1357
|
+
function addMeApiKeysSubcommand(me) {
|
|
1358
|
+
const apiKeys = me.command("api-keys").description("Manage your API keys");
|
|
1359
|
+
const list = apiKeys.command("list").description("List your API keys").action(
|
|
1360
|
+
withErrorHandler(async (_opts, cmd) => {
|
|
1361
|
+
const client = createClient(cmd);
|
|
1362
|
+
const format = getFormat(cmd);
|
|
1363
|
+
const response = await client.rawRequest("GET", "/me/api-keys");
|
|
1364
|
+
outputResponse(response, format);
|
|
1365
|
+
})
|
|
1366
|
+
);
|
|
1367
|
+
addExamples(list, [
|
|
1368
|
+
{
|
|
1369
|
+
description: "List your API keys",
|
|
1370
|
+
command: "geonic me api-keys list"
|
|
1371
|
+
}
|
|
1372
|
+
]);
|
|
1373
|
+
const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Allowed scopes (comma-separated)").option("--origins <origins>", "Allowed origins (comma-separated)").option("--entity-types <types>", "Allowed entity types (comma-separated)").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--save", "Save the API key to config for automatic use").action(
|
|
1374
|
+
withErrorHandler(async (json, _opts, cmd) => {
|
|
1375
|
+
const opts = cmd.opts();
|
|
1376
|
+
if (opts.origins !== void 0) {
|
|
1377
|
+
const parsed = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1378
|
+
if (parsed.length === 0) {
|
|
1379
|
+
printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
|
|
1380
|
+
process.exit(1);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
let body;
|
|
1384
|
+
if (json) {
|
|
1385
|
+
body = await parseJsonInput(json);
|
|
1386
|
+
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0) {
|
|
1387
|
+
const payload = {};
|
|
1388
|
+
if (opts.name) payload.name = opts.name;
|
|
1389
|
+
if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1390
|
+
if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1391
|
+
if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1392
|
+
if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
|
|
1393
|
+
if (opts.rateLimit) {
|
|
1394
|
+
const raw = opts.rateLimit.trim();
|
|
1395
|
+
if (!/^\d+$/.test(raw)) {
|
|
1396
|
+
printError("--rate-limit must be a positive integer.");
|
|
1397
|
+
process.exit(1);
|
|
1398
|
+
}
|
|
1399
|
+
const perMinute = Number(raw);
|
|
1400
|
+
if (perMinute <= 0) {
|
|
1401
|
+
printError("--rate-limit must be a positive integer.");
|
|
1402
|
+
process.exit(1);
|
|
1403
|
+
}
|
|
1404
|
+
payload.rateLimit = { perMinute };
|
|
1405
|
+
}
|
|
1406
|
+
body = payload;
|
|
1407
|
+
} else {
|
|
1408
|
+
body = await parseJsonInput();
|
|
1409
|
+
}
|
|
1410
|
+
if (body && typeof body === "object" && "allowedOrigins" in body) {
|
|
1411
|
+
const origins = body.allowedOrigins;
|
|
1412
|
+
if (Array.isArray(origins) && origins.filter((o) => typeof o === "string" && o.trim() !== "").length === 0) {
|
|
1413
|
+
printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
|
|
1414
|
+
process.exit(1);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const client = createClient(cmd);
|
|
1418
|
+
const format = getFormat(cmd);
|
|
1419
|
+
const response = await client.rawRequest("POST", "/me/api-keys", { body });
|
|
1420
|
+
const data = response.data;
|
|
1421
|
+
if (opts.save) {
|
|
1422
|
+
const globalOpts = resolveOptions(cmd);
|
|
1423
|
+
const key = data.key;
|
|
1424
|
+
if (!key) {
|
|
1425
|
+
printError("Response missing key. API key was created, but it could not be saved.");
|
|
1426
|
+
outputResponse(response, format);
|
|
1427
|
+
process.exitCode = 1;
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const config = loadConfig(globalOpts.profile);
|
|
1431
|
+
config.apiKey = key;
|
|
1432
|
+
saveConfig(config, globalOpts.profile);
|
|
1433
|
+
console.error("API key saved to config. X-Api-Key header will be sent automatically.");
|
|
1434
|
+
} else {
|
|
1435
|
+
printWarning("Save the API key now \u2014 it will not be shown again. Use --save to store it automatically.");
|
|
1436
|
+
}
|
|
1437
|
+
outputResponse(response, format);
|
|
1438
|
+
console.error("API key created.");
|
|
1439
|
+
})
|
|
1440
|
+
);
|
|
1441
|
+
addExamples(create, [
|
|
1442
|
+
{
|
|
1443
|
+
description: "Create an API key with flags",
|
|
1444
|
+
command: "geonic me api-keys create --name my-app --scopes read:entities --origins 'https://example.com'"
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
description: "Create and save API key to config",
|
|
1448
|
+
command: "geonic me api-keys create --name my-app --save"
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
description: "Create an API key from JSON",
|
|
1452
|
+
command: `geonic me api-keys create '{"name":"my-app","allowedScopes":["read:entities"]}'`
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
description: "Create an API key with rate limiting",
|
|
1456
|
+
command: "geonic me api-keys create --name my-app --rate-limit 100"
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
description: "Create an API key with DPoP required",
|
|
1460
|
+
command: "geonic me api-keys create --name my-app --dpop-required"
|
|
1461
|
+
}
|
|
1462
|
+
]);
|
|
1463
|
+
const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
|
|
1464
|
+
withErrorHandler(async (keyId, _opts, cmd) => {
|
|
1465
|
+
const client = createClient(cmd);
|
|
1466
|
+
await client.rawRequest(
|
|
1467
|
+
"DELETE",
|
|
1468
|
+
`/me/api-keys/${encodeURIComponent(String(keyId))}`
|
|
1469
|
+
);
|
|
1470
|
+
console.error("API key deleted.");
|
|
1471
|
+
})
|
|
1472
|
+
);
|
|
1473
|
+
addExamples(del, [
|
|
1474
|
+
{
|
|
1475
|
+
description: "Delete an API key",
|
|
1476
|
+
command: "geonic me api-keys delete <key-id>"
|
|
1477
|
+
}
|
|
1478
|
+
]);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1346
1481
|
// src/commands/auth.ts
|
|
1347
1482
|
function createLoginCommand() {
|
|
1348
1483
|
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(
|
|
@@ -1470,6 +1605,117 @@ function createMeAction() {
|
|
|
1470
1605
|
printInfo(`Profile: ${profileName}`);
|
|
1471
1606
|
});
|
|
1472
1607
|
}
|
|
1608
|
+
async function fetchNonce(baseUrl, apiKey) {
|
|
1609
|
+
const origin = new URL(baseUrl).origin;
|
|
1610
|
+
const url = new URL("/auth/nonce", baseUrl).toString();
|
|
1611
|
+
const response = await fetch(url, {
|
|
1612
|
+
method: "POST",
|
|
1613
|
+
headers: {
|
|
1614
|
+
"Content-Type": "application/json",
|
|
1615
|
+
"Origin": origin
|
|
1616
|
+
},
|
|
1617
|
+
body: JSON.stringify({ api_key: apiKey })
|
|
1618
|
+
});
|
|
1619
|
+
if (!response.ok) {
|
|
1620
|
+
const text = await response.text();
|
|
1621
|
+
throw new Error(`Nonce request failed: ${text || `HTTP ${response.status}`}`);
|
|
1622
|
+
}
|
|
1623
|
+
return await response.json();
|
|
1624
|
+
}
|
|
1625
|
+
function createNonceCommand() {
|
|
1626
|
+
return new Command("nonce").description("Get a nonce and PoW challenge for API key authentication").option("--api-key <key>", "API key to get nonce for").action(
|
|
1627
|
+
withErrorHandler(async (...args) => {
|
|
1628
|
+
const cmd = args[args.length - 1];
|
|
1629
|
+
const nonceOpts = cmd.opts();
|
|
1630
|
+
const globalOpts = resolveOptions(cmd);
|
|
1631
|
+
const apiKey = nonceOpts.apiKey ?? globalOpts.apiKey;
|
|
1632
|
+
if (!apiKey) {
|
|
1633
|
+
printError("API key is required. Use --api-key or configure it with `geonic config set api-key <key>`.");
|
|
1634
|
+
process.exit(1);
|
|
1635
|
+
}
|
|
1636
|
+
if (!globalOpts.url) {
|
|
1637
|
+
printError("No URL configured. Use `geonic config set url <url>` or pass --url.");
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
const baseUrl = validateUrl(globalOpts.url);
|
|
1641
|
+
const data = await fetchNonce(baseUrl, apiKey);
|
|
1642
|
+
const format = getFormat(cmd);
|
|
1643
|
+
outputResponse({ status: 200, headers: new Headers(), data }, format);
|
|
1644
|
+
})
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
function hasLeadingZeroBits(hash, bits) {
|
|
1648
|
+
const fullBytes = Math.floor(bits / 8);
|
|
1649
|
+
const remainingBits = bits % 8;
|
|
1650
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
1651
|
+
if (hash[i] !== 0) return false;
|
|
1652
|
+
}
|
|
1653
|
+
if (remainingBits > 0) {
|
|
1654
|
+
const mask = 255 << 8 - remainingBits;
|
|
1655
|
+
if ((hash[fullBytes] & mask) !== 0) return false;
|
|
1656
|
+
}
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
var MAX_POW_ITERATIONS = 1e7;
|
|
1660
|
+
function solvePoW(challenge, difficulty) {
|
|
1661
|
+
for (let nonce = 0; nonce < MAX_POW_ITERATIONS; nonce++) {
|
|
1662
|
+
const hash = createHash("sha256").update(`${challenge}${nonce}`).digest();
|
|
1663
|
+
if (hasLeadingZeroBits(hash, difficulty)) return nonce;
|
|
1664
|
+
}
|
|
1665
|
+
throw new Error(`PoW could not be solved within ${MAX_POW_ITERATIONS} iterations`);
|
|
1666
|
+
}
|
|
1667
|
+
function createTokenExchangeCommand() {
|
|
1668
|
+
return new Command("token-exchange").description("Exchange API key for a session JWT via nonce + PoW").option("--api-key <key>", "API key to exchange").option("--save", "Save the obtained token to profile config").action(
|
|
1669
|
+
withErrorHandler(async (...args) => {
|
|
1670
|
+
const cmd = args[args.length - 1];
|
|
1671
|
+
const exchangeOpts = cmd.opts();
|
|
1672
|
+
const globalOpts = resolveOptions(cmd);
|
|
1673
|
+
const apiKey = exchangeOpts.apiKey ?? globalOpts.apiKey;
|
|
1674
|
+
if (!apiKey) {
|
|
1675
|
+
printError("API key is required. Use --api-key or configure it with `geonic config set api-key <key>`.");
|
|
1676
|
+
process.exit(1);
|
|
1677
|
+
}
|
|
1678
|
+
if (!globalOpts.url) {
|
|
1679
|
+
printError("No URL configured. Use `geonic config set url <url>` or pass --url.");
|
|
1680
|
+
process.exit(1);
|
|
1681
|
+
}
|
|
1682
|
+
const baseUrl = validateUrl(globalOpts.url);
|
|
1683
|
+
const origin = new URL(baseUrl).origin;
|
|
1684
|
+
const nonceData = await fetchNonce(baseUrl, apiKey);
|
|
1685
|
+
printInfo(`Nonce received. Solving PoW (difficulty=${nonceData.difficulty})...`);
|
|
1686
|
+
const powNonce = solvePoW(nonceData.challenge, nonceData.difficulty);
|
|
1687
|
+
const tokenUrl = new URL("/oauth/token", baseUrl).toString();
|
|
1688
|
+
const tokenResponse = await fetch(tokenUrl, {
|
|
1689
|
+
method: "POST",
|
|
1690
|
+
headers: {
|
|
1691
|
+
"Content-Type": "application/json",
|
|
1692
|
+
"Origin": origin
|
|
1693
|
+
},
|
|
1694
|
+
body: JSON.stringify({
|
|
1695
|
+
grant_type: "api_key",
|
|
1696
|
+
api_key: apiKey,
|
|
1697
|
+
nonce: nonceData.nonce,
|
|
1698
|
+
proof: String(powNonce)
|
|
1699
|
+
})
|
|
1700
|
+
});
|
|
1701
|
+
if (!tokenResponse.ok) {
|
|
1702
|
+
const text = await tokenResponse.text();
|
|
1703
|
+
throw new Error(`Token exchange failed: ${text || `HTTP ${tokenResponse.status}`}`);
|
|
1704
|
+
}
|
|
1705
|
+
const tokenData = await tokenResponse.json();
|
|
1706
|
+
if (exchangeOpts.save) {
|
|
1707
|
+
const config = loadConfig(globalOpts.profile);
|
|
1708
|
+
config.token = tokenData.access_token;
|
|
1709
|
+
saveConfig(config, globalOpts.profile);
|
|
1710
|
+
printSuccess("Token exchange successful. Token saved to config.");
|
|
1711
|
+
} else {
|
|
1712
|
+
const format = getFormat(cmd);
|
|
1713
|
+
outputResponse({ status: tokenResponse.status, headers: tokenResponse.headers, data: tokenData }, format);
|
|
1714
|
+
printSuccess("Token exchange successful.");
|
|
1715
|
+
}
|
|
1716
|
+
})
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1473
1719
|
function registerAuthCommands(program2) {
|
|
1474
1720
|
const auth = program2.command("auth").description("Manage authentication");
|
|
1475
1721
|
const login = createLoginCommand();
|
|
@@ -1500,6 +1746,22 @@ function registerAuthCommands(program2) {
|
|
|
1500
1746
|
}
|
|
1501
1747
|
]);
|
|
1502
1748
|
auth.addCommand(logout);
|
|
1749
|
+
const nonce = createNonceCommand();
|
|
1750
|
+
addExamples(nonce, [
|
|
1751
|
+
{
|
|
1752
|
+
description: "Get a nonce for API key authentication",
|
|
1753
|
+
command: "geonic auth nonce --api-key gdb_abcdef..."
|
|
1754
|
+
}
|
|
1755
|
+
]);
|
|
1756
|
+
auth.addCommand(nonce);
|
|
1757
|
+
const tokenExchange = createTokenExchangeCommand();
|
|
1758
|
+
addExamples(tokenExchange, [
|
|
1759
|
+
{
|
|
1760
|
+
description: "Exchange API key for a JWT and save it",
|
|
1761
|
+
command: "geonic auth token-exchange --api-key gdb_abcdef... --save"
|
|
1762
|
+
}
|
|
1763
|
+
]);
|
|
1764
|
+
auth.addCommand(tokenExchange);
|
|
1503
1765
|
const me = program2.command("me").description("Display current authenticated user and manage user resources");
|
|
1504
1766
|
const meInfo = me.command("info", { isDefault: true, hidden: true }).description("Display current authenticated user").action(createMeAction());
|
|
1505
1767
|
addExamples(me, [
|
|
@@ -1510,6 +1772,10 @@ function registerAuthCommands(program2) {
|
|
|
1510
1772
|
{
|
|
1511
1773
|
description: "List your OAuth clients",
|
|
1512
1774
|
command: "geonic me oauth-clients list"
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
description: "List your API keys",
|
|
1778
|
+
command: "geonic me api-keys list"
|
|
1513
1779
|
}
|
|
1514
1780
|
]);
|
|
1515
1781
|
addExamples(meInfo, [
|
|
@@ -1519,6 +1785,7 @@ function registerAuthCommands(program2) {
|
|
|
1519
1785
|
}
|
|
1520
1786
|
]);
|
|
1521
1787
|
addMeOAuthClientsSubcommand(me);
|
|
1788
|
+
addMeApiKeysSubcommand(me);
|
|
1522
1789
|
program2.addCommand(createLoginCommand(), { hidden: true });
|
|
1523
1790
|
program2.addCommand(createLogoutCommand(), { hidden: true });
|
|
1524
1791
|
const hiddenWhoami = new Command("whoami").description("Display current authenticated user").action(createMeAction());
|
|
@@ -3065,6 +3332,203 @@ function registerCaddeCommand(parent) {
|
|
|
3065
3332
|
]);
|
|
3066
3333
|
}
|
|
3067
3334
|
|
|
3335
|
+
// src/commands/admin/api-keys.ts
|
|
3336
|
+
function validateOrigins(body, opts) {
|
|
3337
|
+
if (opts.origins !== void 0) {
|
|
3338
|
+
const origins = String(opts.origins).split(",").map((s) => s.trim()).filter(Boolean);
|
|
3339
|
+
if (origins.length === 0) {
|
|
3340
|
+
printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
|
|
3341
|
+
process.exit(1);
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
if (body && typeof body === "object" && "allowedOrigins" in body) {
|
|
3345
|
+
const origins = body.allowedOrigins;
|
|
3346
|
+
if (Array.isArray(origins) && origins.filter((o) => typeof o === "string" && o.trim() !== "").length === 0) {
|
|
3347
|
+
printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
|
|
3348
|
+
process.exit(1);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
function buildBodyFromFlags(opts) {
|
|
3353
|
+
const payload = {};
|
|
3354
|
+
if (opts.name) payload.name = opts.name;
|
|
3355
|
+
if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3356
|
+
if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3357
|
+
if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3358
|
+
if (opts.rateLimit) {
|
|
3359
|
+
const raw = String(opts.rateLimit).trim();
|
|
3360
|
+
if (!/^\d+$/.test(raw)) {
|
|
3361
|
+
printError("--rate-limit must be a positive integer.");
|
|
3362
|
+
process.exit(1);
|
|
3363
|
+
}
|
|
3364
|
+
const perMinute = Number(raw);
|
|
3365
|
+
if (perMinute <= 0) {
|
|
3366
|
+
printError("--rate-limit must be a positive integer.");
|
|
3367
|
+
process.exit(1);
|
|
3368
|
+
}
|
|
3369
|
+
payload.rateLimit = { perMinute };
|
|
3370
|
+
}
|
|
3371
|
+
if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
|
|
3372
|
+
if (opts.tenantId) payload.tenantId = opts.tenantId;
|
|
3373
|
+
return payload;
|
|
3374
|
+
}
|
|
3375
|
+
function registerApiKeysCommand(parent) {
|
|
3376
|
+
const apiKeys = parent.command("api-keys").description("Manage API keys");
|
|
3377
|
+
const list = apiKeys.command("list").description("List all API keys").option("--tenant-id <id>", "Filter by tenant ID").action(
|
|
3378
|
+
withErrorHandler(async (_opts, cmd) => {
|
|
3379
|
+
const opts = cmd.opts();
|
|
3380
|
+
const client = createClient(cmd);
|
|
3381
|
+
const format = getFormat(cmd);
|
|
3382
|
+
const params = {};
|
|
3383
|
+
if (opts.tenantId) params.tenantId = opts.tenantId;
|
|
3384
|
+
const response = await client.rawRequest("GET", "/admin/api-keys", {
|
|
3385
|
+
params
|
|
3386
|
+
});
|
|
3387
|
+
outputResponse(response, format);
|
|
3388
|
+
})
|
|
3389
|
+
);
|
|
3390
|
+
addExamples(list, [
|
|
3391
|
+
{
|
|
3392
|
+
description: "List all API keys",
|
|
3393
|
+
command: "geonic admin api-keys list"
|
|
3394
|
+
},
|
|
3395
|
+
{
|
|
3396
|
+
description: "List API keys for a specific tenant",
|
|
3397
|
+
command: "geonic admin api-keys list --tenant-id <tenant-id>"
|
|
3398
|
+
}
|
|
3399
|
+
]);
|
|
3400
|
+
const get = apiKeys.command("get <keyId>").description("Get an API key by ID").action(
|
|
3401
|
+
withErrorHandler(async (keyId, _opts, cmd) => {
|
|
3402
|
+
const client = createClient(cmd);
|
|
3403
|
+
const format = getFormat(cmd);
|
|
3404
|
+
const response = await client.rawRequest(
|
|
3405
|
+
"GET",
|
|
3406
|
+
`/admin/api-keys/${encodeURIComponent(String(keyId))}`
|
|
3407
|
+
);
|
|
3408
|
+
outputResponse(response, format);
|
|
3409
|
+
})
|
|
3410
|
+
);
|
|
3411
|
+
addExamples(get, [
|
|
3412
|
+
{
|
|
3413
|
+
description: "Get an API key by ID",
|
|
3414
|
+
command: "geonic admin api-keys get <key-id>"
|
|
3415
|
+
}
|
|
3416
|
+
]);
|
|
3417
|
+
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(
|
|
3418
|
+
withErrorHandler(async (json, _opts, cmd) => {
|
|
3419
|
+
const opts = cmd.opts();
|
|
3420
|
+
validateOrigins(void 0, opts);
|
|
3421
|
+
let body;
|
|
3422
|
+
if (json) {
|
|
3423
|
+
body = await parseJsonInput(json);
|
|
3424
|
+
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.tenantId) {
|
|
3425
|
+
body = buildBodyFromFlags(opts);
|
|
3426
|
+
} else {
|
|
3427
|
+
body = await parseJsonInput();
|
|
3428
|
+
}
|
|
3429
|
+
validateOrigins(body, {});
|
|
3430
|
+
const client = createClient(cmd);
|
|
3431
|
+
const format = getFormat(cmd);
|
|
3432
|
+
const response = await client.rawRequest("POST", "/admin/api-keys", {
|
|
3433
|
+
body
|
|
3434
|
+
});
|
|
3435
|
+
const data = response.data;
|
|
3436
|
+
if (opts.save) {
|
|
3437
|
+
const globalOpts = resolveOptions(cmd);
|
|
3438
|
+
const key = data.key;
|
|
3439
|
+
if (!key) {
|
|
3440
|
+
printError("Response missing key. API key was created, but it could not be saved.");
|
|
3441
|
+
outputResponse(response, format);
|
|
3442
|
+
process.exitCode = 1;
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
const config = loadConfig(globalOpts.profile);
|
|
3446
|
+
config.apiKey = key;
|
|
3447
|
+
saveConfig(config, globalOpts.profile);
|
|
3448
|
+
console.error("API key saved to config. X-Api-Key header will be sent automatically.");
|
|
3449
|
+
} else {
|
|
3450
|
+
printWarning("Save the API key now \u2014 it will not be shown again. Use --save to store it automatically.");
|
|
3451
|
+
}
|
|
3452
|
+
outputResponse(response, format);
|
|
3453
|
+
console.error("API key created.");
|
|
3454
|
+
})
|
|
3455
|
+
);
|
|
3456
|
+
addExamples(create, [
|
|
3457
|
+
{
|
|
3458
|
+
description: "Create an API key with flags",
|
|
3459
|
+
command: "geonic admin api-keys create --name my-key --scopes entities:read,entities:write --origins '*'"
|
|
3460
|
+
},
|
|
3461
|
+
{
|
|
3462
|
+
description: "Create an API key with DPoP required",
|
|
3463
|
+
command: "geonic admin api-keys create --name my-key --dpop-required"
|
|
3464
|
+
},
|
|
3465
|
+
{
|
|
3466
|
+
description: "Create an API key from JSON and save to config",
|
|
3467
|
+
command: "geonic admin api-keys create @key.json --save"
|
|
3468
|
+
}
|
|
3469
|
+
]);
|
|
3470
|
+
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(
|
|
3471
|
+
withErrorHandler(
|
|
3472
|
+
async (keyId, json, _opts, cmd) => {
|
|
3473
|
+
const opts = cmd.opts();
|
|
3474
|
+
validateOrigins(void 0, opts);
|
|
3475
|
+
let body;
|
|
3476
|
+
if (json) {
|
|
3477
|
+
body = await parseJsonInput(json);
|
|
3478
|
+
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0) {
|
|
3479
|
+
body = buildBodyFromFlags(opts);
|
|
3480
|
+
} else {
|
|
3481
|
+
body = await parseJsonInput();
|
|
3482
|
+
}
|
|
3483
|
+
validateOrigins(body, {});
|
|
3484
|
+
const client = createClient(cmd);
|
|
3485
|
+
const format = getFormat(cmd);
|
|
3486
|
+
const response = await client.rawRequest(
|
|
3487
|
+
"PATCH",
|
|
3488
|
+
`/admin/api-keys/${encodeURIComponent(String(keyId))}`,
|
|
3489
|
+
{ body }
|
|
3490
|
+
);
|
|
3491
|
+
outputResponse(response, format);
|
|
3492
|
+
console.error("API key updated.");
|
|
3493
|
+
}
|
|
3494
|
+
)
|
|
3495
|
+
);
|
|
3496
|
+
addExamples(update, [
|
|
3497
|
+
{
|
|
3498
|
+
description: "Update an API key name",
|
|
3499
|
+
command: "geonic admin api-keys update <key-id> --name new-name"
|
|
3500
|
+
},
|
|
3501
|
+
{
|
|
3502
|
+
description: "Enable DPoP requirement",
|
|
3503
|
+
command: "geonic admin api-keys update <key-id> --dpop-required"
|
|
3504
|
+
},
|
|
3505
|
+
{
|
|
3506
|
+
description: "Disable DPoP requirement",
|
|
3507
|
+
command: "geonic admin api-keys update <key-id> --no-dpop-required"
|
|
3508
|
+
},
|
|
3509
|
+
{
|
|
3510
|
+
description: "Update an API key from a JSON file",
|
|
3511
|
+
command: "geonic admin api-keys update <key-id> @key.json"
|
|
3512
|
+
}
|
|
3513
|
+
]);
|
|
3514
|
+
const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
|
|
3515
|
+
withErrorHandler(async (keyId, _opts, cmd) => {
|
|
3516
|
+
const client = createClient(cmd);
|
|
3517
|
+
await client.rawRequest(
|
|
3518
|
+
"DELETE",
|
|
3519
|
+
`/admin/api-keys/${encodeURIComponent(String(keyId))}`
|
|
3520
|
+
);
|
|
3521
|
+
console.error("API key deleted.");
|
|
3522
|
+
})
|
|
3523
|
+
);
|
|
3524
|
+
addExamples(del, [
|
|
3525
|
+
{
|
|
3526
|
+
description: "Delete an API key",
|
|
3527
|
+
command: "geonic admin api-keys delete <key-id>"
|
|
3528
|
+
}
|
|
3529
|
+
]);
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3068
3532
|
// src/commands/admin/index.ts
|
|
3069
3533
|
function registerAdminCommand(program2) {
|
|
3070
3534
|
const admin = program2.command("admin").description("Manage admin resources");
|
|
@@ -3072,6 +3536,7 @@ function registerAdminCommand(program2) {
|
|
|
3072
3536
|
registerUsersCommand(admin);
|
|
3073
3537
|
registerPoliciesCommand(admin);
|
|
3074
3538
|
registerOAuthClientsCommand(admin);
|
|
3539
|
+
registerApiKeysCommand(admin);
|
|
3075
3540
|
registerCaddeCommand(admin);
|
|
3076
3541
|
}
|
|
3077
3542
|
|