@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.
- package/dist/chunk-AGHP6MNV.js +0 -0
- package/dist/chunk-JNBFNWUF.js +0 -0
- package/dist/{ssh-key-ABJCJDH7.js → chunk-KVBHBOED.js} +2 -1
- package/dist/cli.js +518 -25
- package/dist/cli.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/{server-4FD7U4DZ.js → server-RJQH5B5F.js} +2 -2
- package/dist/ssh-key-Q7KG4K25.js +8 -0
- package/dist/ssh-key-Q7KG4K25.js.map +1 -0
- package/package.json +13 -12
- package/LICENSE +0 -21
- /package/dist/{ssh-key-ABJCJDH7.js.map → chunk-KVBHBOED.js.map} +0 -0
- /package/dist/{server-4FD7U4DZ.js.map → server-RJQH5B5F.js.map} +0 -0
package/dist/chunk-AGHP6MNV.js
CHANGED
|
File without changes
|
package/dist/chunk-JNBFNWUF.js
CHANGED
|
File without changes
|
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
|
|
23
|
-
import { defineCommand as
|
|
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((
|
|
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
|
-
|
|
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-
|
|
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 =
|
|
177
|
-
const privateKey =
|
|
178
|
-
const signature =
|
|
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((
|
|
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
|
|
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:
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2223
|
+
var main = defineCommand25({
|
|
1734
2224
|
meta: {
|
|
1735
2225
|
name: "apes",
|
|
1736
|
-
version: "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
|
-
|
|
2247
|
+
consola22.error(err);
|
|
1755
2248
|
} else {
|
|
1756
|
-
|
|
2249
|
+
consola22.error(err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err));
|
|
1757
2250
|
}
|
|
1758
2251
|
process.exit(1);
|
|
1759
2252
|
});
|