@geolonia/geonicdb-cli 0.5.0 → 0.6.1
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 +46 -3
- package/dist/index.js +94 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,7 +121,47 @@ geonic help [<command>] [<subcommand>]
|
|
|
121
121
|
| `auth nonce` | Get a nonce and PoW challenge for API key authentication |
|
|
122
122
|
| `auth token-exchange` | Exchange API key for a session JWT via nonce + PoW |
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
#### Email/Password Login
|
|
125
|
+
|
|
126
|
+
`auth login` uses interactive prompts for email and password. A TTY is required — credentials are never accepted via environment variables or command-line arguments to prevent leaking secrets in shell history.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
geonic auth login
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
| Option | Description |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `--tenant-id <id>` | Log in to a specific tenant |
|
|
135
|
+
|
|
136
|
+
**Multi-tenant support**: When you belong to multiple tenants, `auth login` displays the list and lets you select one interactively. Use `--tenant-id` to skip the prompt.
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
$ geonic auth login
|
|
140
|
+
Email: user@example.com
|
|
141
|
+
Password: ********
|
|
142
|
+
Login successful. Token saved to config.
|
|
143
|
+
|
|
144
|
+
Available tenants:
|
|
145
|
+
* 1) my_city (tenant_admin) ← current
|
|
146
|
+
2) another_city (user)
|
|
147
|
+
|
|
148
|
+
Select tenant number (Enter to keep current):
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### OAuth Client Credentials
|
|
152
|
+
|
|
153
|
+
For machine-to-machine authentication (CI/CD, scripts), use the OAuth Client Credentials flow:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
geonic auth login --client-credentials --client-id MY_ID --client-secret MY_SECRET
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
| Option | Description |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `--client-credentials` | Use OAuth 2.0 Client Credentials flow |
|
|
162
|
+
| `--client-id <id>` | OAuth client ID (or `GDB_OAUTH_CLIENT_ID` env var) |
|
|
163
|
+
| `--client-secret <secret>` | OAuth client secret (or `GDB_OAUTH_CLIENT_SECRET` env var) |
|
|
164
|
+
| `--scope <scopes>` | OAuth scopes (space-separated) |
|
|
125
165
|
|
|
126
166
|
#### API Key Token Exchange
|
|
127
167
|
|
|
@@ -172,6 +212,7 @@ Displays the current authenticated user, token expiry, and active profile.
|
|
|
172
212
|
| `--origins <origins>` | Allowed origins (comma-separated, at least 1 required) |
|
|
173
213
|
| `--entity-types <types>` | Allowed entity types (comma-separated) |
|
|
174
214
|
| `--rate-limit <n>` | Rate limit (requests per minute) |
|
|
215
|
+
| `--dpop-required` | Require DPoP token binding (RFC 9449) |
|
|
175
216
|
| `--save` | Save the API key to profile config |
|
|
176
217
|
|
|
177
218
|
```bash
|
|
@@ -182,6 +223,8 @@ geonic me api-keys create --name my-app --scopes read:entities --save
|
|
|
182
223
|
geonic me api-keys create '{"name":"my-app","allowedScopes":["read:entities"]}'
|
|
183
224
|
```
|
|
184
225
|
|
|
226
|
+
`me api-keys list` output includes a `dpopRequired` field (boolean).
|
|
227
|
+
|
|
185
228
|
### entities — Manage context entities
|
|
186
229
|
|
|
187
230
|
| Subcommand | Description |
|
|
@@ -369,9 +412,9 @@ Temporal entityOperations query supports: `--aggr-methods`, `--aggr-period`.
|
|
|
369
412
|
| `admin api-keys update <keyId> [json]` | Update an API key |
|
|
370
413
|
| `admin api-keys delete <keyId>` | Delete an API key |
|
|
371
414
|
|
|
372
|
-
`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`, `--tenant-id`, `--save`.
|
|
415
|
+
`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`.
|
|
373
416
|
|
|
374
|
-
**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.
|
|
417
|
+
**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).
|
|
375
418
|
|
|
376
419
|
#### admin cadde
|
|
377
420
|
|
package/dist/index.js
CHANGED
|
@@ -848,6 +848,9 @@ var GdbClient = class _GdbClient {
|
|
|
848
848
|
async executeRawRequest(method, path, options) {
|
|
849
849
|
const url = this.buildUrl(path, options?.params);
|
|
850
850
|
const headers = this.buildHeaders(options?.headers);
|
|
851
|
+
if (options?.skipTenantHeader) {
|
|
852
|
+
delete headers["NGSILD-Tenant"];
|
|
853
|
+
}
|
|
851
854
|
const body = options?.body ? JSON.stringify(options.body) : void 0;
|
|
852
855
|
this.logRequest(method, url, headers, body);
|
|
853
856
|
this.handleDryRun(method, url, headers, body);
|
|
@@ -985,7 +988,7 @@ function withErrorHandler(fn) {
|
|
|
985
988
|
return;
|
|
986
989
|
}
|
|
987
990
|
if (err instanceof GdbClientError && err.status === 401) {
|
|
988
|
-
printError("Authentication failed. Please
|
|
991
|
+
printError("Authentication failed. Please re-authenticate (e.g., `geonic auth login` or check your API key).");
|
|
989
992
|
} else if (err instanceof GdbClientError && err.status === 403) {
|
|
990
993
|
const detail = (err.ngsiError?.detail ?? err.ngsiError?.description ?? "").toLowerCase();
|
|
991
994
|
if (detail.includes("entity type") || detail.includes("allowedentitytypes")) {
|
|
@@ -1097,6 +1100,30 @@ async function promptPassword() {
|
|
|
1097
1100
|
stdin.on("error", onError);
|
|
1098
1101
|
});
|
|
1099
1102
|
}
|
|
1103
|
+
async function promptTenantSelection(tenants, currentTenantId) {
|
|
1104
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1105
|
+
try {
|
|
1106
|
+
console.log("\nAvailable tenants:");
|
|
1107
|
+
for (let i = 0; i < tenants.length; i++) {
|
|
1108
|
+
const t = tenants[i];
|
|
1109
|
+
const current = t.tenantId === currentTenantId ? " \u2190 current" : "";
|
|
1110
|
+
const marker = t.tenantId === currentTenantId ? " *" : " ";
|
|
1111
|
+
console.log(`${marker} ${i + 1}) ${t.tenantId} (${t.role})${current}`);
|
|
1112
|
+
}
|
|
1113
|
+
for (; ; ) {
|
|
1114
|
+
const answer = await rl.question("\nSelect tenant number (Enter to keep current): ");
|
|
1115
|
+
const trimmed = answer.trim();
|
|
1116
|
+
if (!trimmed) return void 0;
|
|
1117
|
+
const index = parseInt(trimmed, 10) - 1;
|
|
1118
|
+
if (index >= 0 && index < tenants.length) {
|
|
1119
|
+
return tenants[index].tenantId;
|
|
1120
|
+
}
|
|
1121
|
+
console.log(`Invalid selection. Please enter a number between 1 and ${tenants.length}.`);
|
|
1122
|
+
}
|
|
1123
|
+
} finally {
|
|
1124
|
+
rl.close();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1100
1127
|
|
|
1101
1128
|
// src/token.ts
|
|
1102
1129
|
function decodeJwtPayload(token) {
|
|
@@ -1370,7 +1397,7 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1370
1397
|
command: "geonic me api-keys list"
|
|
1371
1398
|
}
|
|
1372
1399
|
]);
|
|
1373
|
-
const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Allowed scopes (comma-separated)").option("--origins <origins>", "Allowed origins (comma-separated)").option("--entity-types <types>", "Allowed entity types (comma-separated)").option("--rate-limit <n>", "Rate limit per minute").option("--save", "Save the API key to config for automatic use").action(
|
|
1400
|
+
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
1401
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
1375
1402
|
const opts = cmd.opts();
|
|
1376
1403
|
if (opts.origins !== void 0) {
|
|
@@ -1383,12 +1410,13 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1383
1410
|
let body;
|
|
1384
1411
|
if (json) {
|
|
1385
1412
|
body = await parseJsonInput(json);
|
|
1386
|
-
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit) {
|
|
1413
|
+
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0) {
|
|
1387
1414
|
const payload = {};
|
|
1388
1415
|
if (opts.name) payload.name = opts.name;
|
|
1389
1416
|
if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1390
1417
|
if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1391
1418
|
if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1419
|
+
if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
|
|
1392
1420
|
if (opts.rateLimit) {
|
|
1393
1421
|
const raw = opts.rateLimit.trim();
|
|
1394
1422
|
if (!/^\d+$/.test(raw)) {
|
|
@@ -1453,6 +1481,10 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1453
1481
|
{
|
|
1454
1482
|
description: "Create an API key with rate limiting",
|
|
1455
1483
|
command: "geonic me api-keys create --name my-app --rate-limit 100"
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
description: "Create an API key with DPoP required",
|
|
1487
|
+
command: "geonic me api-keys create --name my-app --dpop-required"
|
|
1456
1488
|
}
|
|
1457
1489
|
]);
|
|
1458
1490
|
const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
|
|
@@ -1506,32 +1538,59 @@ function createLoginCommand() {
|
|
|
1506
1538
|
printSuccess("Login successful (OAuth Client Credentials). Token saved to config.");
|
|
1507
1539
|
return;
|
|
1508
1540
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
if (isInteractive()) {
|
|
1513
|
-
if (!email) email = await promptEmail();
|
|
1514
|
-
if (!password) password = await promptPassword();
|
|
1515
|
-
} else {
|
|
1516
|
-
printError(
|
|
1517
|
-
"Set GDB_EMAIL and GDB_PASSWORD environment variables, or run in a terminal for interactive login."
|
|
1518
|
-
);
|
|
1519
|
-
process.exit(1);
|
|
1520
|
-
}
|
|
1541
|
+
if (!globalOpts.url) {
|
|
1542
|
+
printError("No URL configured. Use `geonic config set url <url>` or pass --url.");
|
|
1543
|
+
process.exit(1);
|
|
1521
1544
|
}
|
|
1545
|
+
try {
|
|
1546
|
+
validateUrl(globalOpts.url);
|
|
1547
|
+
} catch (err) {
|
|
1548
|
+
printError(err.message);
|
|
1549
|
+
process.exit(1);
|
|
1550
|
+
}
|
|
1551
|
+
if (!isInteractive()) {
|
|
1552
|
+
printError(
|
|
1553
|
+
"Interactive terminal required. Run `geonic auth login` in a terminal with TTY."
|
|
1554
|
+
);
|
|
1555
|
+
process.exit(1);
|
|
1556
|
+
}
|
|
1557
|
+
const email = await promptEmail();
|
|
1558
|
+
const password = await promptPassword();
|
|
1522
1559
|
const client = createClient(cmd);
|
|
1523
1560
|
const body = { email, password };
|
|
1524
1561
|
if (loginOpts.tenantId) {
|
|
1525
1562
|
body.tenantId = loginOpts.tenantId;
|
|
1526
1563
|
}
|
|
1527
|
-
const response = await client.rawRequest("POST", "/auth/login", {
|
|
1564
|
+
const response = await client.rawRequest("POST", "/auth/login", {
|
|
1565
|
+
body,
|
|
1566
|
+
skipTenantHeader: true
|
|
1567
|
+
});
|
|
1528
1568
|
const data = response.data;
|
|
1529
|
-
|
|
1530
|
-
|
|
1569
|
+
let token = data.accessToken ?? data.token;
|
|
1570
|
+
let refreshToken = data.refreshToken;
|
|
1531
1571
|
if (!token) {
|
|
1532
1572
|
printError("No token received from server.");
|
|
1533
1573
|
process.exit(1);
|
|
1534
1574
|
}
|
|
1575
|
+
const availableTenants = data.availableTenants;
|
|
1576
|
+
const currentTenantId = data.tenantId;
|
|
1577
|
+
if (availableTenants && availableTenants.length > 1 && !loginOpts.tenantId) {
|
|
1578
|
+
const selectedTenantId = await promptTenantSelection(availableTenants, currentTenantId);
|
|
1579
|
+
if (selectedTenantId && selectedTenantId !== currentTenantId) {
|
|
1580
|
+
const reloginResponse = await client.rawRequest("POST", "/auth/login", {
|
|
1581
|
+
body: { email, password, tenantId: selectedTenantId },
|
|
1582
|
+
skipTenantHeader: true
|
|
1583
|
+
});
|
|
1584
|
+
const reloginData = reloginResponse.data;
|
|
1585
|
+
const newToken = reloginData.accessToken ?? reloginData.token;
|
|
1586
|
+
if (!newToken) {
|
|
1587
|
+
printError("Re-login failed: no token received for selected tenant.");
|
|
1588
|
+
process.exit(1);
|
|
1589
|
+
}
|
|
1590
|
+
token = newToken;
|
|
1591
|
+
refreshToken = reloginData.refreshToken;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1535
1594
|
const config = loadConfig(globalOpts.profile);
|
|
1536
1595
|
config.token = token;
|
|
1537
1596
|
if (refreshToken) {
|
|
@@ -1723,10 +1782,6 @@ function registerAuthCommands(program2) {
|
|
|
1723
1782
|
description: "Login with OAuth client credentials",
|
|
1724
1783
|
command: "geonic auth login --client-credentials --client-id MY_ID --client-secret MY_SECRET"
|
|
1725
1784
|
},
|
|
1726
|
-
{
|
|
1727
|
-
description: "Login with environment variables",
|
|
1728
|
-
command: "GDB_EMAIL=user@example.com GDB_PASSWORD=pass geonic auth login"
|
|
1729
|
-
},
|
|
1730
1785
|
{
|
|
1731
1786
|
description: "Login to a specific tenant",
|
|
1732
1787
|
command: "geonic auth login --tenant-id my-tenant"
|
|
@@ -3363,6 +3418,7 @@ function buildBodyFromFlags(opts) {
|
|
|
3363
3418
|
}
|
|
3364
3419
|
payload.rateLimit = { perMinute };
|
|
3365
3420
|
}
|
|
3421
|
+
if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
|
|
3366
3422
|
if (opts.tenantId) payload.tenantId = opts.tenantId;
|
|
3367
3423
|
return payload;
|
|
3368
3424
|
}
|
|
@@ -3408,14 +3464,14 @@ function registerApiKeysCommand(parent) {
|
|
|
3408
3464
|
command: "geonic admin api-keys get <key-id>"
|
|
3409
3465
|
}
|
|
3410
3466
|
]);
|
|
3411
|
-
const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Comma-separated scopes").option("--origins <origins>", "Comma-separated origins").option("--entity-types <types>", "Comma-separated entity types").option("--rate-limit <n>", "Rate limit per minute").option("--tenant-id <id>", "Tenant ID").option("--save", "Save the API key to profile config").action(
|
|
3467
|
+
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(
|
|
3412
3468
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
3413
3469
|
const opts = cmd.opts();
|
|
3414
3470
|
validateOrigins(void 0, opts);
|
|
3415
3471
|
let body;
|
|
3416
3472
|
if (json) {
|
|
3417
3473
|
body = await parseJsonInput(json);
|
|
3418
|
-
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.tenantId) {
|
|
3474
|
+
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.tenantId) {
|
|
3419
3475
|
body = buildBodyFromFlags(opts);
|
|
3420
3476
|
} else {
|
|
3421
3477
|
body = await parseJsonInput();
|
|
@@ -3452,12 +3508,16 @@ function registerApiKeysCommand(parent) {
|
|
|
3452
3508
|
description: "Create an API key with flags",
|
|
3453
3509
|
command: "geonic admin api-keys create --name my-key --scopes entities:read,entities:write --origins '*'"
|
|
3454
3510
|
},
|
|
3511
|
+
{
|
|
3512
|
+
description: "Create an API key with DPoP required",
|
|
3513
|
+
command: "geonic admin api-keys create --name my-key --dpop-required"
|
|
3514
|
+
},
|
|
3455
3515
|
{
|
|
3456
3516
|
description: "Create an API key from JSON and save to config",
|
|
3457
3517
|
command: "geonic admin api-keys create @key.json --save"
|
|
3458
3518
|
}
|
|
3459
3519
|
]);
|
|
3460
|
-
const update = apiKeys.command("update <keyId> [json]").description("Update an API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Comma-separated scopes").option("--origins <origins>", "Comma-separated origins").option("--entity-types <types>", "Comma-separated entity types").option("--rate-limit <n>", "Rate limit per minute").action(
|
|
3520
|
+
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(
|
|
3461
3521
|
withErrorHandler(
|
|
3462
3522
|
async (keyId, json, _opts, cmd) => {
|
|
3463
3523
|
const opts = cmd.opts();
|
|
@@ -3465,7 +3525,7 @@ function registerApiKeysCommand(parent) {
|
|
|
3465
3525
|
let body;
|
|
3466
3526
|
if (json) {
|
|
3467
3527
|
body = await parseJsonInput(json);
|
|
3468
|
-
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit) {
|
|
3528
|
+
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0) {
|
|
3469
3529
|
body = buildBodyFromFlags(opts);
|
|
3470
3530
|
} else {
|
|
3471
3531
|
body = await parseJsonInput();
|
|
@@ -3488,6 +3548,14 @@ function registerApiKeysCommand(parent) {
|
|
|
3488
3548
|
description: "Update an API key name",
|
|
3489
3549
|
command: "geonic admin api-keys update <key-id> --name new-name"
|
|
3490
3550
|
},
|
|
3551
|
+
{
|
|
3552
|
+
description: "Enable DPoP requirement",
|
|
3553
|
+
command: "geonic admin api-keys update <key-id> --dpop-required"
|
|
3554
|
+
},
|
|
3555
|
+
{
|
|
3556
|
+
description: "Disable DPoP requirement",
|
|
3557
|
+
command: "geonic admin api-keys update <key-id> --no-dpop-required"
|
|
3558
|
+
},
|
|
3491
3559
|
{
|
|
3492
3560
|
description: "Update an API key from a JSON file",
|
|
3493
3561
|
command: "geonic admin api-keys update <key-id> @key.json"
|