@openape/apes 0.3.0 → 0.5.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.
File without changes
File without changes
@@ -73,7 +73,8 @@ function parseOpenSSHEd25519(pem) {
73
73
  format: "jwk"
74
74
  });
75
75
  }
76
+
76
77
  export {
77
78
  loadEd25519PrivateKey
78
79
  };
79
- //# sourceMappingURL=ssh-key-ABJCJDH7.js.map
80
+ //# sourceMappingURL=chunk-KVBHBOED.js.map
package/dist/cli.js CHANGED
@@ -2,6 +2,9 @@
2
2
  import {
3
3
  parseDuration
4
4
  } from "./chunk-AGHP6MNV.js";
5
+ import {
6
+ loadEd25519PrivateKey
7
+ } from "./chunk-KVBHBOED.js";
5
8
  import {
6
9
  ApiError,
7
10
  apiFetch,
@@ -19,8 +22,8 @@ import {
19
22
  } from "./chunk-JNBFNWUF.js";
20
23
 
21
24
  // src/cli.ts
22
- import consola19 from "consola";
23
- import { defineCommand as defineCommand22, runMain } from "citty";
25
+ import consola22 from "consola";
26
+ import { defineCommand as defineCommand25, runMain } from "citty";
24
27
 
25
28
  // src/commands/auth/login.ts
26
29
  import { Buffer } from "buffer";
@@ -84,7 +87,7 @@ async function loginWithPKCE(idp) {
84
87
  authUrl.searchParams.set("state", state);
85
88
  authUrl.searchParams.set("nonce", nonce);
86
89
  authUrl.searchParams.set("scope", "openid email profile offline_access");
87
- const code = await new Promise((resolve, reject) => {
90
+ const code = await new Promise((resolve2, reject) => {
88
91
  const server = createServer((req, res) => {
89
92
  const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
90
93
  if (url.pathname === "/callback") {
@@ -101,7 +104,7 @@ async function loginWithPKCE(idp) {
101
104
  res.writeHead(200, { "Content-Type": "text/html" });
102
105
  res.end("<h1>Login successful!</h1><p>You can close this window.</p>");
103
106
  server.close();
104
- resolve(authCode);
107
+ resolve2(authCode);
105
108
  return;
106
109
  }
107
110
  res.writeHead(400);
@@ -154,9 +157,9 @@ async function loginWithPKCE(idp) {
154
157
  consola.success(`Logged in as ${payload.email || payload.sub}`);
155
158
  }
156
159
  async function loginWithKey(idp, keyPath, email) {
157
- const { readFileSync } = await import("fs");
158
- const { sign } = await import("crypto");
159
- const { loadEd25519PrivateKey } = await import("./ssh-key-ABJCJDH7.js");
160
+ const { readFileSync: readFileSync2 } = await import("fs");
161
+ const { sign: sign2 } = await import("crypto");
162
+ const { loadEd25519PrivateKey: loadEd25519PrivateKey2 } = await import("./ssh-key-Q7KG4K25.js");
160
163
  const agentEmail = email;
161
164
  if (!agentEmail) {
162
165
  consola.error("Agent email required for key-based login. Use --email <agent-email>");
@@ -173,9 +176,9 @@ async function loginWithKey(idp, keyPath, email) {
173
176
  return process.exit(1);
174
177
  }
175
178
  const { challenge } = await challengeResp.json();
176
- const keyContent = readFileSync(keyPath, "utf-8");
177
- const privateKey = loadEd25519PrivateKey(keyContent);
178
- const signature = sign(null, Buffer.from(challenge), privateKey).toString("base64");
179
+ const keyContent = readFileSync2(keyPath, "utf-8");
180
+ const privateKey = loadEd25519PrivateKey2(keyContent);
181
+ const signature = sign2(null, Buffer.from(challenge), privateKey).toString("base64");
179
182
  const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
180
183
  const authResp = await fetch(authenticateUrl, {
181
184
  method: "POST",
@@ -644,7 +647,7 @@ async function waitForApproval2(grantsUrl, grantId) {
644
647
  consola7.error("Grant revoked.");
645
648
  process.exit(1);
646
649
  }
647
- await new Promise((resolve) => setTimeout(resolve, interval));
650
+ await new Promise((resolve2) => setTimeout(resolve2, interval));
648
651
  }
649
652
  consola7.error("Timed out waiting for approval.");
650
653
  process.exit(1);
@@ -654,6 +657,55 @@ var requestCapabilityCommand = defineCommand8({
654
657
  name: "request-capability",
655
658
  description: "Request a structured CLI capability grant"
656
659
  },
660
+ args: {
661
+ "cliId": {
662
+ type: "positional",
663
+ description: "CLI adapter identifier (e.g. docker, kubectl)",
664
+ required: true
665
+ },
666
+ "resource": {
667
+ type: "string",
668
+ description: "Resource scope (repeatable)"
669
+ },
670
+ "selector": {
671
+ type: "string",
672
+ description: "Selector filter (repeatable)"
673
+ },
674
+ "action": {
675
+ type: "string",
676
+ description: "Action to permit (repeatable)"
677
+ },
678
+ "adapter": {
679
+ type: "string",
680
+ description: "Explicit path to adapter TOML file"
681
+ },
682
+ "idp": {
683
+ type: "string",
684
+ description: "IdP URL"
685
+ },
686
+ "approval": {
687
+ type: "string",
688
+ description: "Approval type: once, timed, always",
689
+ default: "once"
690
+ },
691
+ "reason": {
692
+ type: "string",
693
+ description: "Reason for the request"
694
+ },
695
+ "duration": {
696
+ type: "string",
697
+ description: "Duration for timed grants (e.g. 30m, 1h, 7d)"
698
+ },
699
+ "run-as": {
700
+ type: "string",
701
+ description: "Execute as this user (e.g. root)"
702
+ },
703
+ "wait": {
704
+ type: "boolean",
705
+ description: "Wait for approval before returning",
706
+ default: false
707
+ }
708
+ },
657
709
  async run({ rawArgs }) {
658
710
  const auth = loadAuth();
659
711
  if (!auth) {
@@ -754,22 +806,72 @@ import consola10 from "consola";
754
806
  var revokeCommand = defineCommand11({
755
807
  meta: {
756
808
  name: "revoke",
757
- description: "Revoke a grant"
809
+ description: "Revoke one or more grants"
758
810
  },
759
811
  args: {
760
812
  id: {
761
813
  type: "positional",
762
- description: "Grant ID",
763
- required: true
814
+ description: "Grant ID(s) to revoke",
815
+ required: false
816
+ },
817
+ allPending: {
818
+ type: "boolean",
819
+ description: "Revoke all own pending grants",
820
+ default: false
764
821
  }
765
822
  },
766
823
  async run({ args }) {
767
824
  const idp = getIdpUrl();
768
825
  const grantsUrl = await getGrantsEndpoint(idp);
769
- await apiFetch(`${grantsUrl}/${args.id}/revoke`, {
770
- method: "POST"
771
- });
772
- consola10.success(`Grant ${args.id} revoked.`);
826
+ const explicitIds = args.id ? [String(args.id), ...args._].filter(Boolean) : [];
827
+ if (args.allPending && explicitIds.length > 0) {
828
+ consola10.error("Use either --all-pending or grant IDs, not both.");
829
+ return process.exit(1);
830
+ }
831
+ let ids;
832
+ if (args.allPending) {
833
+ const auth = loadAuth();
834
+ const response = await apiFetch(
835
+ `${grantsUrl}?status=pending&limit=100`
836
+ );
837
+ const ownPending = auth?.email ? response.data.filter((g) => g.requester === auth.email) : response.data;
838
+ if (ownPending.length === 0) {
839
+ consola10.info("No pending grants to revoke.");
840
+ return;
841
+ }
842
+ ids = ownPending.map((g) => g.id);
843
+ consola10.info(`Found ${ids.length} pending grant(s) to revoke.`);
844
+ } else if (explicitIds.length > 0) {
845
+ ids = explicitIds;
846
+ } else {
847
+ consola10.error("Provide grant ID(s) or use --all-pending.");
848
+ return process.exit(1);
849
+ }
850
+ if (ids.length === 1) {
851
+ await apiFetch(`${grantsUrl}/${ids[0]}/revoke`, { method: "POST" });
852
+ consola10.success(`Grant ${ids[0]} revoked.`);
853
+ return;
854
+ }
855
+ const operations = ids.map((id) => ({ id, action: "revoke" }));
856
+ const { results } = await apiFetch(
857
+ `${grantsUrl}/batch`,
858
+ { method: "POST", body: { operations } }
859
+ );
860
+ let succeeded = 0;
861
+ for (const r of results) {
862
+ if (r.success) {
863
+ consola10.success(`Grant ${r.id} revoked.`);
864
+ succeeded++;
865
+ } else {
866
+ consola10.error(`Grant ${r.id}: ${r.error?.title || "Failed"}`);
867
+ }
868
+ }
869
+ if (succeeded < results.length) {
870
+ consola10.info(`Revoked ${succeeded} of ${results.length} grants.`);
871
+ process.exit(1);
872
+ } else {
873
+ consola10.success(`All ${succeeded} grants revoked.`);
874
+ }
773
875
  }
774
876
  });
775
877
 
@@ -1355,6 +1457,16 @@ async function runAdapterMode(command, rawArgs, args) {
1355
1457
  });
1356
1458
  consola15.info(`Grant requested: ${grant.id}`);
1357
1459
  consola15.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
1460
+ if (grant.similar_grants?.similar_grants?.length) {
1461
+ const n = grant.similar_grants.similar_grants.length;
1462
+ consola15.info("");
1463
+ consola15.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
1464
+ if (grant.similar_grants.widened_details?.length) {
1465
+ const wider = grant.similar_grants.widened_details.map((d) => d.permission).join(", ");
1466
+ consola15.info(` Broader scope: ${wider}`);
1467
+ }
1468
+ consola15.info("");
1469
+ }
1358
1470
  const status = await waitForGrantStatus(idp, grant.id);
1359
1471
  if (status !== "approved")
1360
1472
  throw new Error(`Grant ${status}`);
@@ -1694,14 +1806,392 @@ var mcpCommand = defineCommand21({
1694
1806
  if (transport !== "stdio" && transport !== "sse") {
1695
1807
  throw new Error('Transport must be "stdio" or "sse"');
1696
1808
  }
1697
- const { startMcpServer } = await import("./server-4FD7U4DZ.js");
1809
+ const { startMcpServer } = await import("./server-RJQH5B5F.js");
1698
1810
  await startMcpServer(transport, port);
1699
1811
  }
1700
1812
  });
1701
1813
 
1814
+ // src/commands/init/index.ts
1815
+ import { existsSync, copyFileSync, writeFileSync } from "fs";
1816
+ import { randomBytes } from "crypto";
1817
+ import { execFileSync as execFileSync2 } from "child_process";
1818
+ import { join } from "path";
1819
+ import { defineCommand as defineCommand22 } from "citty";
1820
+ import consola19 from "consola";
1821
+ var DEFAULT_IDP_URL = "https://id.openape.at";
1822
+ async function downloadTemplate(repo, targetDir) {
1823
+ const { downloadTemplate: gigetDownload } = await import("giget");
1824
+ await gigetDownload(`gh:${repo}`, { dir: targetDir, force: false });
1825
+ }
1826
+ function installDeps(dir) {
1827
+ const hasLockFile = (name) => existsSync(join(dir, name));
1828
+ if (hasLockFile("pnpm-lock.yaml")) {
1829
+ execFileSync2("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
1830
+ } else if (hasLockFile("bun.lockb")) {
1831
+ execFileSync2("bun", ["install"], { cwd: dir, stdio: "inherit" });
1832
+ } else {
1833
+ execFileSync2("npm", ["install"], { cwd: dir, stdio: "inherit" });
1834
+ }
1835
+ }
1836
+ async function promptChoice(message, choices) {
1837
+ const result = await consola19.prompt(message, { type: "select", options: choices });
1838
+ if (typeof result === "symbol") {
1839
+ process.exit(0);
1840
+ }
1841
+ return result;
1842
+ }
1843
+ async function promptText(message, defaultValue) {
1844
+ const result = await consola19.prompt(message, { type: "text", default: defaultValue, placeholder: defaultValue });
1845
+ if (typeof result === "symbol") {
1846
+ process.exit(0);
1847
+ }
1848
+ return result || defaultValue || "";
1849
+ }
1850
+ var initCommand = defineCommand22({
1851
+ meta: {
1852
+ name: "init",
1853
+ description: "Scaffold a new OpenApe project"
1854
+ },
1855
+ args: {
1856
+ sp: {
1857
+ type: "boolean",
1858
+ description: "Create a Service Provider app"
1859
+ },
1860
+ idp: {
1861
+ type: "boolean",
1862
+ description: "Create an Identity Provider app"
1863
+ },
1864
+ dir: {
1865
+ type: "positional",
1866
+ description: "Target directory",
1867
+ required: false
1868
+ }
1869
+ },
1870
+ async run({ args }) {
1871
+ let mode;
1872
+ if (args.sp) {
1873
+ mode = "sp";
1874
+ } else if (args.idp) {
1875
+ mode = "idp";
1876
+ } else {
1877
+ const choice = await promptChoice("What do you want to set up?", [
1878
+ "SP \u2014 Add login to my app",
1879
+ "IdP \u2014 Run my own Identity Provider"
1880
+ ]);
1881
+ mode = choice.startsWith("SP") ? "sp" : "idp";
1882
+ }
1883
+ if (mode === "sp") {
1884
+ await initSP(args.dir);
1885
+ } else {
1886
+ await initIdP(args.dir);
1887
+ }
1888
+ }
1889
+ });
1890
+ async function initSP(targetDir) {
1891
+ const dir = targetDir || "my-app";
1892
+ if (existsSync(join(dir, "package.json"))) {
1893
+ consola19.error(`Directory "${dir}" already contains a project.`);
1894
+ return process.exit(1);
1895
+ }
1896
+ consola19.start("Scaffolding SP starter...");
1897
+ await downloadTemplate("openape-ai/openape-sp-starter", dir);
1898
+ consola19.success("Scaffolded from openape-sp-starter");
1899
+ consola19.start("Installing dependencies...");
1900
+ installDeps(dir);
1901
+ consola19.success("Dependencies installed");
1902
+ const envExample = join(dir, ".env.example");
1903
+ const envFile = join(dir, ".env");
1904
+ if (existsSync(envExample) && !existsSync(envFile)) {
1905
+ copyFileSync(envExample, envFile);
1906
+ consola19.success(`\`.env\` created (using Free IdP at ${DEFAULT_IDP_URL})`);
1907
+ }
1908
+ console.log("");
1909
+ consola19.box([
1910
+ `cd ${dir}`,
1911
+ "npm run dev",
1912
+ "",
1913
+ "Then open http://localhost:3001/login"
1914
+ ].join("\n"));
1915
+ }
1916
+ async function initIdP(targetDir) {
1917
+ const dir = targetDir || "my-idp";
1918
+ if (existsSync(join(dir, "package.json"))) {
1919
+ consola19.error(`Directory "${dir}" already contains a project.`);
1920
+ return process.exit(1);
1921
+ }
1922
+ const domain = await promptText("Domain for the IdP", "localhost");
1923
+ const storage = await promptChoice("Storage backend", [
1924
+ "memory (dev only, data lost on restart)",
1925
+ "fs (local filesystem)",
1926
+ "s3 (S3-compatible)"
1927
+ ]);
1928
+ const adminEmail = await promptText("Admin email");
1929
+ consola19.start("Scaffolding IdP starter...");
1930
+ await downloadTemplate("openape-ai/openape-idp-starter", dir);
1931
+ consola19.success("Scaffolded from openape-idp-starter");
1932
+ consola19.start("Installing dependencies...");
1933
+ installDeps(dir);
1934
+ consola19.success("Dependencies installed");
1935
+ const sessionSecret = randomBytes(32).toString("hex");
1936
+ const managementToken = randomBytes(32).toString("hex");
1937
+ consola19.success("Secrets generated");
1938
+ const isLocalhost = domain === "localhost";
1939
+ const origin = isLocalhost ? "http://localhost:3000" : `https://${domain}`;
1940
+ const envContent = [
1941
+ "# Generated by apes init --idp",
1942
+ "",
1943
+ `NUXT_OPENAPE_SESSION_SECRET=${sessionSecret}`,
1944
+ `NUXT_OPENAPE_ADMIN_EMAILS=${adminEmail}`,
1945
+ `NUXT_OPENAPE_MANAGEMENT_TOKEN=${managementToken}`,
1946
+ `NUXT_OPENAPE_ISSUER=${origin}`,
1947
+ `NUXT_OPENAPE_RP_NAME=My Identity Provider`,
1948
+ `NUXT_OPENAPE_RP_ID=${domain}`,
1949
+ `NUXT_OPENAPE_RP_ORIGIN=${origin}`
1950
+ ].join("\n");
1951
+ writeFileSync(join(dir, ".env"), envContent + "\n", { mode: 384 });
1952
+ consola19.success(".env created");
1953
+ console.log("");
1954
+ consola19.box([
1955
+ `cd ${dir}`,
1956
+ "npm run dev",
1957
+ "",
1958
+ "Then open http://localhost:3000/admin",
1959
+ "",
1960
+ ...isLocalhost ? [] : [
1961
+ "For production:",
1962
+ ` 1. DNS TXT Record: _ddisa.${domain.replace(/^id\./, "")} \u2192 "v=ddisa1 idp=${origin}"`,
1963
+ ` 2. Storage: switch to ${storage.includes("s3") ? "s3" : "fs"} in nuxt.config.ts`,
1964
+ " 3. Deploy: vercel deploy"
1965
+ ]
1966
+ ].join("\n"));
1967
+ }
1968
+
1969
+ // src/commands/enroll.ts
1970
+ import { Buffer as Buffer2 } from "buffer";
1971
+ import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2, mkdirSync } from "fs";
1972
+ import { execFile as execFile2 } from "child_process";
1973
+ import { generateKeyPairSync, sign } from "crypto";
1974
+ import { dirname, resolve } from "path";
1975
+ import { homedir } from "os";
1976
+ import { defineCommand as defineCommand23 } from "citty";
1977
+ import consola20 from "consola";
1978
+ var DEFAULT_IDP_URL2 = "https://id.openape.at";
1979
+ var DEFAULT_KEY_PATH = "~/.ssh/id_ed25519";
1980
+ var POLL_INTERVAL = 3e3;
1981
+ var POLL_TIMEOUT = 3e5;
1982
+ function resolvePath(p) {
1983
+ return resolve(p.replace(/^~/, homedir()));
1984
+ }
1985
+ function openBrowser2(url) {
1986
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1987
+ execFile2(cmd, [url], () => {
1988
+ });
1989
+ }
1990
+ function readPublicKey(keyPath) {
1991
+ const pubPath = `${keyPath}.pub`;
1992
+ if (existsSync2(pubPath)) {
1993
+ return readFileSync(pubPath, "utf-8").trim();
1994
+ }
1995
+ const keyContent = readFileSync(keyPath, "utf-8");
1996
+ const privateKey = loadEd25519PrivateKey(keyContent);
1997
+ const jwk = privateKey.export({ format: "jwk" });
1998
+ const pubBytes = Buffer2.from(jwk.x, "base64url");
1999
+ const keyTypeStr = "ssh-ed25519";
2000
+ const keyTypeLen = Buffer2.alloc(4);
2001
+ keyTypeLen.writeUInt32BE(keyTypeStr.length);
2002
+ const pubKeyLen = Buffer2.alloc(4);
2003
+ pubKeyLen.writeUInt32BE(pubBytes.length);
2004
+ const blob = Buffer2.concat([keyTypeLen, Buffer2.from(keyTypeStr), pubKeyLen, pubBytes]);
2005
+ return `ssh-ed25519 ${blob.toString("base64")}`;
2006
+ }
2007
+ function generateAndSaveKey(keyPath) {
2008
+ const resolved = resolvePath(keyPath);
2009
+ const dir = dirname(resolved);
2010
+ if (!existsSync2(dir)) {
2011
+ mkdirSync(dir, { recursive: true });
2012
+ }
2013
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
2014
+ const privatePem = privateKey.export({ type: "pkcs8", format: "pem" });
2015
+ writeFileSync2(resolved, privatePem, { mode: 384 });
2016
+ const jwk = publicKey.export({ format: "jwk" });
2017
+ const pubBytes = Buffer2.from(jwk.x, "base64url");
2018
+ const keyTypeStr = "ssh-ed25519";
2019
+ const keyTypeLen = Buffer2.alloc(4);
2020
+ keyTypeLen.writeUInt32BE(keyTypeStr.length);
2021
+ const pubKeyLen = Buffer2.alloc(4);
2022
+ pubKeyLen.writeUInt32BE(pubBytes.length);
2023
+ const blob = Buffer2.concat([keyTypeLen, Buffer2.from(keyTypeStr), pubKeyLen, pubBytes]);
2024
+ const pubKeyStr = `ssh-ed25519 ${blob.toString("base64")}`;
2025
+ writeFileSync2(`${resolved}.pub`, `${pubKeyStr}
2026
+ `, { mode: 420 });
2027
+ return pubKeyStr;
2028
+ }
2029
+ async function pollForEnrollment(idp, agentEmail, keyPath) {
2030
+ const resolvedKey = resolvePath(keyPath);
2031
+ const keyContent = readFileSync(resolvedKey, "utf-8");
2032
+ const privateKey = loadEd25519PrivateKey(keyContent);
2033
+ const challengeUrl = await getAgentChallengeEndpoint(idp);
2034
+ const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
2035
+ const startTime = Date.now();
2036
+ while (Date.now() - startTime < POLL_TIMEOUT) {
2037
+ try {
2038
+ const challengeResp = await fetch(challengeUrl, {
2039
+ method: "POST",
2040
+ headers: { "Content-Type": "application/json" },
2041
+ body: JSON.stringify({ agent_id: agentEmail })
2042
+ });
2043
+ if (challengeResp.ok) {
2044
+ const { challenge } = await challengeResp.json();
2045
+ const signature = sign(null, Buffer2.from(challenge), privateKey).toString("base64");
2046
+ const authResp = await fetch(authenticateUrl, {
2047
+ method: "POST",
2048
+ headers: { "Content-Type": "application/json" },
2049
+ body: JSON.stringify({ agent_id: agentEmail, challenge, signature })
2050
+ });
2051
+ if (authResp.ok) {
2052
+ const result = await authResp.json();
2053
+ return { token: result.token, expiresIn: result.expires_in };
2054
+ }
2055
+ }
2056
+ } catch {
2057
+ }
2058
+ await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL));
2059
+ }
2060
+ throw new Error("Enrollment timed out. Please check the browser and try again.");
2061
+ }
2062
+ var enrollCommand = defineCommand23({
2063
+ meta: {
2064
+ name: "enroll",
2065
+ description: "Enroll an agent with an Identity Provider"
2066
+ },
2067
+ args: {
2068
+ idp: {
2069
+ type: "string",
2070
+ description: `IdP URL (default: ${DEFAULT_IDP_URL2})`
2071
+ },
2072
+ name: {
2073
+ type: "string",
2074
+ description: "Agent name"
2075
+ },
2076
+ key: {
2077
+ type: "string",
2078
+ description: `Path to Ed25519 key (default: ${DEFAULT_KEY_PATH})`
2079
+ }
2080
+ },
2081
+ async run({ args }) {
2082
+ const idp = args.idp || await consola20.prompt("IdP URL", { type: "text", default: DEFAULT_IDP_URL2, placeholder: DEFAULT_IDP_URL2 }).then((r) => typeof r === "symbol" ? process.exit(0) : r) || DEFAULT_IDP_URL2;
2083
+ const agentName = args.name || await consola20.prompt("Agent name", { type: "text", placeholder: "deploy-bot" }).then((r) => typeof r === "symbol" ? process.exit(0) : r);
2084
+ if (!agentName) {
2085
+ consola20.error("Agent name is required.");
2086
+ return process.exit(1);
2087
+ }
2088
+ const keyPath = args.key || await consola20.prompt("Ed25519 key", { type: "text", default: DEFAULT_KEY_PATH, placeholder: DEFAULT_KEY_PATH }).then((r) => typeof r === "symbol" ? process.exit(0) : r) || DEFAULT_KEY_PATH;
2089
+ const resolvedKey = resolvePath(keyPath);
2090
+ let publicKey;
2091
+ if (existsSync2(resolvedKey)) {
2092
+ publicKey = readPublicKey(resolvedKey);
2093
+ consola20.success(`Using existing key ${keyPath}`);
2094
+ } else {
2095
+ consola20.start(`Generating Ed25519 key pair at ${keyPath}...`);
2096
+ publicKey = generateAndSaveKey(keyPath);
2097
+ consola20.success(`Key pair generated at ${keyPath}`);
2098
+ }
2099
+ const encodedKey = encodeURIComponent(publicKey);
2100
+ const enrollUrl = `${idp}/enroll?name=${encodeURIComponent(agentName)}&key=${encodedKey}`;
2101
+ consola20.info("Opening browser for enrollment...");
2102
+ consola20.info(`\u2192 ${idp}/enroll`);
2103
+ openBrowser2(enrollUrl);
2104
+ console.log("");
2105
+ const agentEmail = await consola20.prompt(
2106
+ "Agent email (shown in browser after enrollment)",
2107
+ { type: "text", placeholder: `agent+${agentName}@...` }
2108
+ ).then((r) => typeof r === "symbol" ? process.exit(0) : r);
2109
+ if (!agentEmail) {
2110
+ consola20.error("Agent email is required to verify enrollment.");
2111
+ return process.exit(1);
2112
+ }
2113
+ consola20.start("Verifying enrollment...");
2114
+ const { token, expiresIn } = await pollForEnrollment(idp, agentEmail, keyPath);
2115
+ saveAuth({
2116
+ idp,
2117
+ access_token: token,
2118
+ email: agentEmail,
2119
+ expires_at: Math.floor(Date.now() / 1e3) + (expiresIn || 3600)
2120
+ });
2121
+ const config = loadConfig();
2122
+ config.defaults = { ...config.defaults, idp };
2123
+ config.agent = { key: keyPath, email: agentEmail };
2124
+ saveConfig(config);
2125
+ consola20.success(`Agent enrolled as ${agentEmail}`);
2126
+ consola20.success("Config saved to ~/.config/apes/");
2127
+ console.log("");
2128
+ consola20.info("Verify with: apes whoami");
2129
+ }
2130
+ });
2131
+
2132
+ // src/commands/dns-check.ts
2133
+ import { defineCommand as defineCommand24 } from "citty";
2134
+ import consola21 from "consola";
2135
+ import { resolveDDISA } from "@openape/core";
2136
+ var dnsCheckCommand = defineCommand24({
2137
+ meta: {
2138
+ name: "dns-check",
2139
+ description: "Validate DDISA DNS TXT records for a domain"
2140
+ },
2141
+ args: {
2142
+ domain: {
2143
+ type: "positional",
2144
+ description: "Domain to check (e.g. example.com)",
2145
+ required: true
2146
+ }
2147
+ },
2148
+ async run({ args }) {
2149
+ const domain = args.domain;
2150
+ consola21.start(`Checking _ddisa.${domain}...`);
2151
+ try {
2152
+ const result = await resolveDDISA(domain);
2153
+ if (!result) {
2154
+ consola21.error(`No DDISA record found for ${domain}`);
2155
+ console.log("");
2156
+ console.log("To set up DDISA, add a DNS TXT record:");
2157
+ console.log(` _ddisa.${domain} TXT "v=ddisa1 idp=https://id.${domain}"`);
2158
+ return process.exit(1);
2159
+ }
2160
+ consola21.success(`_ddisa.${domain} \u2192 ${result.idp}`);
2161
+ console.log("");
2162
+ console.log(` Version: ${result.version || "ddisa1"}`);
2163
+ console.log(` IdP URL: ${result.idp}`);
2164
+ if (result.mode)
2165
+ console.log(` Mode: ${result.mode}`);
2166
+ if (result.priority !== void 0)
2167
+ console.log(` Priority: ${result.priority}`);
2168
+ console.log("");
2169
+ consola21.start(`Verifying IdP at ${result.idp}...`);
2170
+ const discoResp = await fetch(`${result.idp}/.well-known/openid-configuration`);
2171
+ if (!discoResp.ok) {
2172
+ consola21.warn(`IdP discovery failed (${discoResp.status}). Is the IdP running at ${result.idp}?`);
2173
+ return;
2174
+ }
2175
+ const disco = await discoResp.json();
2176
+ consola21.success(`IdP is reachable`);
2177
+ console.log(` Issuer: ${disco.issuer}`);
2178
+ console.log(` DDISA: v${disco.ddisa_version || "?"}`);
2179
+ if (disco.ddisa_auth_methods_supported) {
2180
+ console.log(` Auth: ${disco.ddisa_auth_methods_supported.join(", ")}`);
2181
+ }
2182
+ if (disco.openape_grant_types_supported) {
2183
+ console.log(` Grants: ${disco.openape_grant_types_supported.join(", ")}`);
2184
+ }
2185
+ } catch (err) {
2186
+ consola21.error(`DNS check failed: ${err instanceof Error ? err.message : String(err)}`);
2187
+ return process.exit(1);
2188
+ }
2189
+ }
2190
+ });
2191
+
1702
2192
  // src/cli.ts
1703
2193
  var debug = process.argv.includes("--debug");
1704
- var grantsCommand = defineCommand22({
2194
+ var grantsCommand = defineCommand25({
1705
2195
  meta: {
1706
2196
  name: "grants",
1707
2197
  description: "Grant management"
@@ -1720,7 +2210,7 @@ var grantsCommand = defineCommand22({
1720
2210
  delegations: delegationsCommand
1721
2211
  }
1722
2212
  });
1723
- var configCommand = defineCommand22({
2213
+ var configCommand = defineCommand25({
1724
2214
  meta: {
1725
2215
  name: "config",
1726
2216
  description: "Configuration management"
@@ -1730,13 +2220,16 @@ var configCommand = defineCommand22({
1730
2220
  set: configSetCommand
1731
2221
  }
1732
2222
  });
1733
- var main = defineCommand22({
2223
+ var main = defineCommand25({
1734
2224
  meta: {
1735
2225
  name: "apes",
1736
- version: "0.3.0",
2226
+ version: "0.5.0",
1737
2227
  description: "Unified CLI for OpenApe"
1738
2228
  },
1739
2229
  subCommands: {
2230
+ init: initCommand,
2231
+ enroll: enrollCommand,
2232
+ "dns-check": dnsCheckCommand,
1740
2233
  login: loginCommand,
1741
2234
  logout: logoutCommand,
1742
2235
  whoami: whoamiCommand,
@@ -1751,9 +2244,9 @@ var main = defineCommand22({
1751
2244
  });
1752
2245
  runMain(main).catch((err) => {
1753
2246
  if (debug) {
1754
- consola19.error(err);
2247
+ consola22.error(err);
1755
2248
  } else {
1756
- consola19.error(err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err));
2249
+ consola22.error(err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err));
1757
2250
  }
1758
2251
  process.exit(1);
1759
2252
  });