@geolonia/geonicdb-cli 0.3.0 → 0.4.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
@@ -584,6 +584,36 @@ function registerConfigCommand(program2) {
584
584
  // src/commands/auth.ts
585
585
  import { Command } from "commander";
586
586
 
587
+ // src/oauth.ts
588
+ async function clientCredentialsGrant(options) {
589
+ const url = new URL("/oauth/token", options.baseUrl).toString();
590
+ const credentials = Buffer.from(`${options.clientId}:${options.clientSecret}`).toString("base64");
591
+ const body = { grant_type: "client_credentials" };
592
+ if (options.scope) {
593
+ body.scope = options.scope;
594
+ }
595
+ const response = await fetch(url, {
596
+ method: "POST",
597
+ headers: {
598
+ "Content-Type": "application/json",
599
+ Authorization: `Basic ${credentials}`
600
+ },
601
+ body: JSON.stringify(body)
602
+ });
603
+ if (!response.ok) {
604
+ const text = await response.text();
605
+ let message;
606
+ try {
607
+ const err = JSON.parse(text);
608
+ message = err.error_description ?? err.error ?? text;
609
+ } catch {
610
+ message = text || `HTTP ${response.status}`;
611
+ }
612
+ throw new Error(`OAuth token request failed: ${message}`);
613
+ }
614
+ return await response.json();
615
+ }
616
+
587
617
  // src/client.ts
588
618
  var DryRunSignal = class extends Error {
589
619
  constructor() {
@@ -597,6 +627,8 @@ var GdbClient = class _GdbClient {
597
627
  token;
598
628
  refreshToken;
599
629
  apiKey;
630
+ clientId;
631
+ clientSecret;
600
632
  onTokenRefresh;
601
633
  verbose;
602
634
  dryRun;
@@ -607,6 +639,8 @@ var GdbClient = class _GdbClient {
607
639
  this.token = options.token;
608
640
  this.refreshToken = options.refreshToken;
609
641
  this.apiKey = options.apiKey;
642
+ this.clientId = options.clientId;
643
+ this.clientSecret = options.clientSecret;
610
644
  this.onTokenRefresh = options.onTokenRefresh;
611
645
  this.verbose = options.verbose ?? false;
612
646
  this.dryRun = options.dryRun ?? false;
@@ -713,7 +747,7 @@ var GdbClient = class _GdbClient {
713
747
  throw new DryRunSignal();
714
748
  }
715
749
  canRefresh() {
716
- return !!this.refreshToken && !this.apiKey;
750
+ return (!!this.refreshToken || !!this.clientId && !!this.clientSecret) && !this.apiKey;
717
751
  }
718
752
  async performTokenRefresh() {
719
753
  if (this.refreshPromise) return this.refreshPromise;
@@ -725,26 +759,43 @@ var GdbClient = class _GdbClient {
725
759
  }
726
760
  }
727
761
  async doRefresh() {
728
- if (!this.refreshToken) return false;
729
- try {
730
- const url = this.buildUrl("/auth/refresh");
731
- const response = await fetch(url, {
732
- method: "POST",
733
- headers: { "Content-Type": "application/json" },
734
- body: JSON.stringify({ refreshToken: this.refreshToken })
735
- });
736
- if (!response.ok) return false;
737
- const data = await response.json();
738
- const newToken = data.accessToken ?? data.token;
739
- const newRefreshToken = data.refreshToken;
740
- if (!newToken) return false;
741
- this.token = newToken;
742
- if (newRefreshToken) this.refreshToken = newRefreshToken;
743
- this.onTokenRefresh?.(newToken, newRefreshToken);
744
- return true;
745
- } catch {
746
- return false;
762
+ if (this.refreshToken) {
763
+ try {
764
+ const url = this.buildUrl("/auth/refresh");
765
+ const response = await fetch(url, {
766
+ method: "POST",
767
+ headers: { "Content-Type": "application/json" },
768
+ body: JSON.stringify({ refreshToken: this.refreshToken })
769
+ });
770
+ if (response.ok) {
771
+ const data = await response.json();
772
+ const newToken = data.accessToken ?? data.token;
773
+ const newRefreshToken = data.refreshToken;
774
+ if (newToken) {
775
+ this.token = newToken;
776
+ if (newRefreshToken) this.refreshToken = newRefreshToken;
777
+ this.onTokenRefresh?.(newToken, newRefreshToken);
778
+ return true;
779
+ }
780
+ }
781
+ } catch {
782
+ }
747
783
  }
784
+ if (this.clientId && this.clientSecret) {
785
+ try {
786
+ const result = await clientCredentialsGrant({
787
+ baseUrl: this.baseUrl,
788
+ clientId: this.clientId,
789
+ clientSecret: this.clientSecret
790
+ });
791
+ this.token = result.access_token;
792
+ this.onTokenRefresh?.(result.access_token);
793
+ return true;
794
+ } catch {
795
+ return false;
796
+ }
797
+ }
798
+ return false;
748
799
  }
749
800
  async executeRequest(method, path, options) {
750
801
  const url = this.buildUrl(`${this.getBasePath()}${path}`, options?.params);
@@ -877,6 +928,8 @@ function createClient(cmd) {
877
928
  service: opts.service,
878
929
  token: opts.token,
879
930
  refreshToken: usingCliToken ? void 0 : config.refreshToken,
931
+ clientId: usingCliToken ? void 0 : config.clientId,
932
+ clientSecret: usingCliToken ? void 0 : config.clientSecret,
880
933
  apiKey: opts.apiKey,
881
934
  onTokenRefresh: usingCliToken ? void 0 : (token, refreshToken) => {
882
935
  const cfg = loadConfig(opts.profile);
@@ -1057,35 +1110,217 @@ function formatDuration(ms) {
1057
1110
  return parts.join(" ");
1058
1111
  }
1059
1112
 
1060
- // src/oauth.ts
1061
- async function clientCredentialsGrant(options) {
1062
- const url = new URL("/oauth/token", options.baseUrl).toString();
1063
- const credentials = Buffer.from(`${options.clientId}:${options.clientSecret}`).toString("base64");
1064
- const params = new URLSearchParams();
1065
- params.set("grant_type", "client_credentials");
1066
- if (options.scope) {
1067
- params.set("scope", options.scope);
1113
+ // src/input.ts
1114
+ import JSON5 from "json5";
1115
+ import { readFileSync as readFileSync2 } from "fs";
1116
+ import { createInterface as createInterface2 } from "readline";
1117
+ async function parseJsonInput(input) {
1118
+ if (input !== void 0) {
1119
+ if (input === "-") return parseData(readFileSync2(0, "utf-8"));
1120
+ if (input.startsWith("@")) return parseData(readFileSync2(input.slice(1), "utf-8"));
1121
+ return parseData(input);
1068
1122
  }
1069
- const response = await fetch(url, {
1070
- method: "POST",
1071
- headers: {
1072
- "Content-Type": "application/x-www-form-urlencoded",
1073
- Authorization: `Basic ${credentials}`
1074
- },
1075
- body: params.toString()
1123
+ if (!process.stdin.isTTY) {
1124
+ return parseData(readFileSync2(0, "utf-8"));
1125
+ }
1126
+ return readInteractiveJson();
1127
+ }
1128
+ function parseData(text) {
1129
+ return JSON5.parse(text.trim());
1130
+ }
1131
+ async function readInteractiveJson() {
1132
+ const rl = createInterface2({
1133
+ input: process.stdin,
1134
+ output: process.stderr,
1135
+ prompt: "json> "
1076
1136
  });
1077
- if (!response.ok) {
1078
- const text = await response.text();
1079
- let message;
1080
- try {
1081
- const err = JSON.parse(text);
1082
- message = err.error_description ?? err.error ?? text;
1083
- } catch {
1084
- message = text || `HTTP ${response.status}`;
1137
+ process.stderr.write("Enter JSON (auto-submits when braces close, Ctrl+C to cancel):\n");
1138
+ rl.prompt();
1139
+ const lines = [];
1140
+ let depth = 0;
1141
+ let started = false;
1142
+ let inBlockComment = false;
1143
+ let inString = false;
1144
+ let stringChar = "";
1145
+ let cancelled = false;
1146
+ return new Promise((resolve, reject) => {
1147
+ rl.on("SIGINT", () => {
1148
+ cancelled = true;
1149
+ rl.close();
1150
+ });
1151
+ rl.on("line", (line) => {
1152
+ lines.push(line);
1153
+ const result = trackDepth(line, depth, started, inBlockComment, inString, stringChar);
1154
+ depth = result.depth;
1155
+ started = result.started;
1156
+ inBlockComment = result.inBlockComment;
1157
+ inString = result.inString;
1158
+ stringChar = result.stringChar;
1159
+ if (started && depth <= 0 && !inBlockComment && !inString) {
1160
+ rl.close();
1161
+ try {
1162
+ resolve(parseData(lines.join("\n")));
1163
+ } catch (err) {
1164
+ reject(err);
1165
+ }
1166
+ } else {
1167
+ rl.setPrompt("... ");
1168
+ rl.prompt();
1169
+ }
1170
+ });
1171
+ rl.on("close", () => {
1172
+ if (cancelled) {
1173
+ reject(new Error("Input cancelled."));
1174
+ return;
1175
+ }
1176
+ if (lines.length > 0 && (!started || depth > 0 || inBlockComment || inString)) {
1177
+ try {
1178
+ resolve(parseData(lines.join("\n")));
1179
+ } catch (err) {
1180
+ reject(err);
1181
+ }
1182
+ } else if (lines.length === 0) {
1183
+ reject(new Error("No input provided."));
1184
+ }
1185
+ });
1186
+ });
1187
+ }
1188
+ function trackDepth(line, depth, started, inBlockComment, inString, stringChar) {
1189
+ for (let i = 0; i < line.length; i++) {
1190
+ const ch = line[i];
1191
+ const next = i + 1 < line.length ? line[i + 1] : "";
1192
+ if (inBlockComment) {
1193
+ if (ch === "*" && next === "/") {
1194
+ inBlockComment = false;
1195
+ i++;
1196
+ }
1197
+ continue;
1198
+ }
1199
+ if (inString) {
1200
+ if (ch === "\\" && i + 1 < line.length) {
1201
+ i++;
1202
+ } else if (ch === stringChar) {
1203
+ inString = false;
1204
+ }
1205
+ continue;
1206
+ }
1207
+ if (ch === "/" && next === "/") break;
1208
+ if (ch === "/" && next === "*") {
1209
+ inBlockComment = true;
1210
+ i++;
1211
+ continue;
1212
+ }
1213
+ if (ch === '"' || ch === "'") {
1214
+ inString = true;
1215
+ stringChar = ch;
1216
+ } else if (ch === "{" || ch === "[") {
1217
+ depth++;
1218
+ started = true;
1219
+ } else if (ch === "}" || ch === "]") {
1220
+ depth--;
1085
1221
  }
1086
- throw new Error(`OAuth token request failed: ${message}`);
1087
1222
  }
1088
- return await response.json();
1223
+ return { depth, started, inBlockComment, inString, stringChar };
1224
+ }
1225
+
1226
+ // src/commands/me-oauth-clients.ts
1227
+ function addMeOAuthClientsSubcommand(me) {
1228
+ const oauthClients = me.command("oauth-clients").description("Manage your OAuth clients");
1229
+ const list = oauthClients.command("list").description("List your OAuth clients").action(
1230
+ withErrorHandler(async (_opts, cmd) => {
1231
+ const client = createClient(cmd);
1232
+ const format = getFormat(cmd);
1233
+ const response = await client.rawRequest("GET", "/me/oauth-clients");
1234
+ outputResponse(response, format);
1235
+ })
1236
+ );
1237
+ addExamples(list, [
1238
+ {
1239
+ description: "List your OAuth clients",
1240
+ command: "geonic me oauth-clients list"
1241
+ }
1242
+ ]);
1243
+ 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(
1244
+ withErrorHandler(async (json, _opts, cmd) => {
1245
+ const opts = cmd.opts();
1246
+ let body;
1247
+ if (json) {
1248
+ body = await parseJsonInput(json);
1249
+ } else if (opts.name || opts.scopes) {
1250
+ const payload = {};
1251
+ if (opts.name) payload.clientName = opts.name;
1252
+ if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim());
1253
+ body = payload;
1254
+ } else {
1255
+ body = await parseJsonInput();
1256
+ }
1257
+ const client = createClient(cmd);
1258
+ const format = getFormat(cmd);
1259
+ const response = await client.rawRequest("POST", "/me/oauth-clients", { body });
1260
+ const data = response.data;
1261
+ if (opts.save) {
1262
+ const globalOpts = resolveOptions(cmd);
1263
+ const clientId = data.clientId;
1264
+ const clientSecret = data.clientSecret;
1265
+ if (!clientId || !clientSecret) {
1266
+ printError("Response missing clientId or clientSecret. Cannot save credentials.");
1267
+ outputResponse(response, format);
1268
+ printSuccess("OAuth client created.");
1269
+ return;
1270
+ }
1271
+ const baseUrl = validateUrl(globalOpts.url);
1272
+ const tokenResult = await clientCredentialsGrant({
1273
+ baseUrl,
1274
+ clientId,
1275
+ clientSecret,
1276
+ scope: data.allowedScopes?.join(" ")
1277
+ });
1278
+ const config = loadConfig(globalOpts.profile);
1279
+ config.clientId = clientId;
1280
+ config.clientSecret = clientSecret;
1281
+ config.token = tokenResult.access_token;
1282
+ delete config.refreshToken;
1283
+ saveConfig(config, globalOpts.profile);
1284
+ printInfo("Client credentials saved to config. Auto-reauth enabled.");
1285
+ } else {
1286
+ printWarning(
1287
+ "Save the clientSecret now \u2014 it will not be shown again."
1288
+ );
1289
+ }
1290
+ outputResponse(response, format);
1291
+ printSuccess("OAuth client created.");
1292
+ })
1293
+ );
1294
+ addExamples(create, [
1295
+ {
1296
+ description: "Create an OAuth client with flags",
1297
+ command: "geonic me oauth-clients create --name my-ci-bot --scopes read:entities,write:entities"
1298
+ },
1299
+ {
1300
+ description: "Create and save credentials for auto-reauth",
1301
+ command: "geonic me oauth-clients create --name my-ci-bot --save"
1302
+ },
1303
+ {
1304
+ description: "Create an OAuth client from JSON",
1305
+ command: `geonic me oauth-clients create '{"clientName":"my-bot","allowedScopes":["read:entities"]}'`
1306
+ }
1307
+ ]);
1308
+ const del = oauthClients.command("delete <id>").description("Delete an OAuth client").action(
1309
+ withErrorHandler(async (id, _opts, cmd) => {
1310
+ const client = createClient(cmd);
1311
+ await client.rawRequest(
1312
+ "DELETE",
1313
+ `/me/oauth-clients/${encodeURIComponent(String(id))}`
1314
+ );
1315
+ printSuccess("OAuth client deleted.");
1316
+ })
1317
+ );
1318
+ addExamples(del, [
1319
+ {
1320
+ description: "Delete an OAuth client",
1321
+ command: "geonic me oauth-clients delete <client-id>"
1322
+ }
1323
+ ]);
1089
1324
  }
1090
1325
 
1091
1326
  // src/commands/auth.ts
@@ -1245,13 +1480,25 @@ function registerAuthCommands(program2) {
1245
1480
  }
1246
1481
  ]);
1247
1482
  auth.addCommand(logout);
1248
- const me = program2.command("me").description("Display current authenticated user").action(createMeAction());
1483
+ const me = program2.command("me").description("Display current authenticated user and manage user resources");
1484
+ const meInfo = me.command("info", { isDefault: true, hidden: true }).description("Display current authenticated user").action(createMeAction());
1249
1485
  addExamples(me, [
1250
1486
  {
1251
1487
  description: "Show current user info",
1252
1488
  command: "geonic me"
1489
+ },
1490
+ {
1491
+ description: "List your OAuth clients",
1492
+ command: "geonic me oauth-clients list"
1253
1493
  }
1254
1494
  ]);
1495
+ addExamples(meInfo, [
1496
+ {
1497
+ description: "Show current user info",
1498
+ command: "geonic me"
1499
+ }
1500
+ ]);
1501
+ addMeOAuthClientsSubcommand(me);
1255
1502
  program2.addCommand(createLoginCommand(), { hidden: true });
1256
1503
  program2.addCommand(createLogoutCommand(), { hidden: true });
1257
1504
  const hiddenWhoami = new Command("whoami").description("Display current authenticated user").action(createMeAction());
@@ -1347,119 +1594,6 @@ function registerProfileCommands(program2) {
1347
1594
  ]);
1348
1595
  }
1349
1596
 
1350
- // src/input.ts
1351
- import JSON5 from "json5";
1352
- import { readFileSync as readFileSync2 } from "fs";
1353
- import { createInterface as createInterface2 } from "readline";
1354
- async function parseJsonInput(input) {
1355
- if (input !== void 0) {
1356
- if (input === "-") return parseData(readFileSync2(0, "utf-8"));
1357
- if (input.startsWith("@")) return parseData(readFileSync2(input.slice(1), "utf-8"));
1358
- return parseData(input);
1359
- }
1360
- if (!process.stdin.isTTY) {
1361
- return parseData(readFileSync2(0, "utf-8"));
1362
- }
1363
- return readInteractiveJson();
1364
- }
1365
- function parseData(text) {
1366
- return JSON5.parse(text.trim());
1367
- }
1368
- async function readInteractiveJson() {
1369
- const rl = createInterface2({
1370
- input: process.stdin,
1371
- output: process.stderr,
1372
- prompt: "json> "
1373
- });
1374
- process.stderr.write("Enter JSON (auto-submits when braces close, Ctrl+C to cancel):\n");
1375
- rl.prompt();
1376
- const lines = [];
1377
- let depth = 0;
1378
- let started = false;
1379
- let inBlockComment = false;
1380
- let inString = false;
1381
- let stringChar = "";
1382
- let cancelled = false;
1383
- return new Promise((resolve, reject) => {
1384
- rl.on("SIGINT", () => {
1385
- cancelled = true;
1386
- rl.close();
1387
- });
1388
- rl.on("line", (line) => {
1389
- lines.push(line);
1390
- const result = trackDepth(line, depth, started, inBlockComment, inString, stringChar);
1391
- depth = result.depth;
1392
- started = result.started;
1393
- inBlockComment = result.inBlockComment;
1394
- inString = result.inString;
1395
- stringChar = result.stringChar;
1396
- if (started && depth <= 0 && !inBlockComment && !inString) {
1397
- rl.close();
1398
- try {
1399
- resolve(parseData(lines.join("\n")));
1400
- } catch (err) {
1401
- reject(err);
1402
- }
1403
- } else {
1404
- rl.setPrompt("... ");
1405
- rl.prompt();
1406
- }
1407
- });
1408
- rl.on("close", () => {
1409
- if (cancelled) {
1410
- reject(new Error("Input cancelled."));
1411
- return;
1412
- }
1413
- if (lines.length > 0 && (!started || depth > 0 || inBlockComment || inString)) {
1414
- try {
1415
- resolve(parseData(lines.join("\n")));
1416
- } catch (err) {
1417
- reject(err);
1418
- }
1419
- } else if (lines.length === 0) {
1420
- reject(new Error("No input provided."));
1421
- }
1422
- });
1423
- });
1424
- }
1425
- function trackDepth(line, depth, started, inBlockComment, inString, stringChar) {
1426
- for (let i = 0; i < line.length; i++) {
1427
- const ch = line[i];
1428
- const next = i + 1 < line.length ? line[i + 1] : "";
1429
- if (inBlockComment) {
1430
- if (ch === "*" && next === "/") {
1431
- inBlockComment = false;
1432
- i++;
1433
- }
1434
- continue;
1435
- }
1436
- if (inString) {
1437
- if (ch === "\\" && i + 1 < line.length) {
1438
- i++;
1439
- } else if (ch === stringChar) {
1440
- inString = false;
1441
- }
1442
- continue;
1443
- }
1444
- if (ch === "/" && next === "/") break;
1445
- if (ch === "/" && next === "*") {
1446
- inBlockComment = true;
1447
- i++;
1448
- continue;
1449
- }
1450
- if (ch === '"' || ch === "'") {
1451
- inString = true;
1452
- stringChar = ch;
1453
- } else if (ch === "{" || ch === "[") {
1454
- depth++;
1455
- started = true;
1456
- } else if (ch === "}" || ch === "]") {
1457
- depth--;
1458
- }
1459
- }
1460
- return { depth, started, inBlockComment, inString, stringChar };
1461
- }
1462
-
1463
1597
  // src/commands/attrs.ts
1464
1598
  function addAttrsSubcommands(attrs) {
1465
1599
  const list = attrs.command("list").description("List all attributes of an entity").argument("<entityId>", "Entity ID").action(