@geolonia/geonicdb-cli 0.6.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 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
- 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`.
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
 
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 run `geonic login` to re-authenticate.");
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) {
@@ -1511,32 +1538,59 @@ function createLoginCommand() {
1511
1538
  printSuccess("Login successful (OAuth Client Credentials). Token saved to config.");
1512
1539
  return;
1513
1540
  }
1514
- let email = process.env.GDB_EMAIL;
1515
- let password = process.env.GDB_PASSWORD;
1516
- if (!email || !password) {
1517
- if (isInteractive()) {
1518
- if (!email) email = await promptEmail();
1519
- if (!password) password = await promptPassword();
1520
- } else {
1521
- printError(
1522
- "Set GDB_EMAIL and GDB_PASSWORD environment variables, or run in a terminal for interactive login."
1523
- );
1524
- process.exit(1);
1525
- }
1541
+ if (!globalOpts.url) {
1542
+ printError("No URL configured. Use `geonic config set url <url>` or pass --url.");
1543
+ process.exit(1);
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);
1526
1556
  }
1557
+ const email = await promptEmail();
1558
+ const password = await promptPassword();
1527
1559
  const client = createClient(cmd);
1528
1560
  const body = { email, password };
1529
1561
  if (loginOpts.tenantId) {
1530
1562
  body.tenantId = loginOpts.tenantId;
1531
1563
  }
1532
- const response = await client.rawRequest("POST", "/auth/login", { body });
1564
+ const response = await client.rawRequest("POST", "/auth/login", {
1565
+ body,
1566
+ skipTenantHeader: true
1567
+ });
1533
1568
  const data = response.data;
1534
- const token = data.accessToken ?? data.token;
1535
- const refreshToken = data.refreshToken;
1569
+ let token = data.accessToken ?? data.token;
1570
+ let refreshToken = data.refreshToken;
1536
1571
  if (!token) {
1537
1572
  printError("No token received from server.");
1538
1573
  process.exit(1);
1539
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
+ }
1540
1594
  const config = loadConfig(globalOpts.profile);
1541
1595
  config.token = token;
1542
1596
  if (refreshToken) {
@@ -1728,10 +1782,6 @@ function registerAuthCommands(program2) {
1728
1782
  description: "Login with OAuth client credentials",
1729
1783
  command: "geonic auth login --client-credentials --client-id MY_ID --client-secret MY_SECRET"
1730
1784
  },
1731
- {
1732
- description: "Login with environment variables",
1733
- command: "GDB_EMAIL=user@example.com GDB_PASSWORD=pass geonic auth login"
1734
- },
1735
1785
  {
1736
1786
  description: "Login to a specific tenant",
1737
1787
  command: "geonic auth login --tenant-id my-tenant"