@rine-network/cli 0.10.2 → 0.11.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/dist/main.js +406 -63
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import { HttpClient, RineApiError, UUID_RE, addMlsGroupMember, agentKeysExist, cacheToken, decryptGroupMessage, decryptMessage, encryptGroupMessage, encryptMessage, encryptMlsGroupMessage, externalJoinMlsGroup, fetchAgents, fetchAndIngestPendingSKDistributions, fetchOAuthToken, fetchRecipientKeys, formatError, fromBase64Url, generateAgentKeys, generatePqKeyPair, getAgentPublicKeys, getCredentialEntry, getOrCreateSenderKey, getOrRefreshToken, ingestSenderKeyDistribution, initMlsGroup, isBareAgentName, listMyInvites, loadAgentKeys, loadCredentials, loadMlsState, loadTokenCache, normalizeHandle, performAgentCreation, performRegistration, processMlsWelcomes, resolveAgent, resolveApiUrl, resolveConfigDir, resolveHandleViaWebFinger, resolveToUuid, saveAgentKeys, saveCredentials, savePqEncryptionKey, saveTokenCache, sendGroupInviteNotification, syncMlsGroup, toBase64Url, validateEncryptionKey, validateSigningKey, validateSlug } from "@rine-network/core";
|
|
3
|
+
import { HttpClient, RineApiError, UUID_RE, addMlsGroupMember, agentKeysExist, buildIssueCertDeps, cacheToken, connectTunnel, decryptGroupMessage, decryptMessage, deleteHookSecret, encryptGroupMessage, encryptMessage, encryptMlsGroupMessage, ensureCert, externalJoinMlsGroup, fetchAgents, fetchAndIngestPendingSKDistributions, fetchOAuthToken, fetchRecipientKeys, formatError, fromBase64Url, generateAgentKeys, generateHookSecret, generatePqKeyPair, getAgentPublicKeys, getCredentialEntry, getOrCreateSenderKey, getOrRefreshToken, handleWebhookRequest, ingestSenderKeyDistribution, initMlsGroup, isBareAgentName, isValidHookName, listMyInvites, loadAgentKeys, loadCredentials, loadHookSecret, loadMlsState, loadTokenCache, normalizeHandle, performAgentCreation, performRegistration, processMlsWelcomes, resolveAgent, resolveApiUrl, resolveConfigDir, resolveHandleViaWebFinger, resolveToUuid, revokeAndWipeCache, saveAgentKeys, saveCredentials, saveHookSecret, savePqEncryptionKey, saveTokenCache, sendGroupInviteNotification, startTlsListener, syncMlsGroup, toBase64Url, validateEncryptionKey, validateSigningKey, validateSlug } from "@rine-network/core";
|
|
4
4
|
import readline from "node:readline";
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
import { createEventSource } from "eventsource-client";
|
|
8
9
|
//#region src/output.ts
|
|
@@ -905,6 +906,106 @@ function registerGroup(program) {
|
|
|
905
906
|
}));
|
|
906
907
|
}
|
|
907
908
|
//#endregion
|
|
909
|
+
//#region src/commands/hook.ts
|
|
910
|
+
const SIGNATURE_HEADER = "X-Hub-Signature-256";
|
|
911
|
+
const CONTENT_TYPE = "application/json";
|
|
912
|
+
function registerHook(program) {
|
|
913
|
+
const hook = program.command("hook").description("Manage inbound webhook funnel hooks");
|
|
914
|
+
hook.command("create").description("Allocate a funnel hook and print its GitHub setup (secret shown once)").option("--agent <id>", "Agent ID (defaults to your agent)").option("--name <name>", "Hook name (default: default)", "default").action(withClient(program, async ({ client, gOpts, apiUrl, configDir }, opts) => {
|
|
915
|
+
if (!isValidHookName(opts.name)) {
|
|
916
|
+
printError(`Invalid hook name '${opts.name}': use lowercase letters, digits and hyphens (max 32).`);
|
|
917
|
+
process.exitCode = 2;
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
921
|
+
const secret = generateHookSecret();
|
|
922
|
+
saveHookSecret(configDir, agentId, opts.name, secret);
|
|
923
|
+
let created;
|
|
924
|
+
try {
|
|
925
|
+
created = await client.post(`/agents/${agentId}/funnel/hooks`, { hook_name: opts.name });
|
|
926
|
+
} catch (err) {
|
|
927
|
+
deleteHookSecret(configDir, agentId, opts.name);
|
|
928
|
+
throw err;
|
|
929
|
+
}
|
|
930
|
+
printCreated(created, opts.name, secret, gOpts.json);
|
|
931
|
+
}));
|
|
932
|
+
hook.command("list").description("List your agent's funnel hooks").option("--agent <id>", "Agent ID (defaults to your agent)").action(withClient(program, async ({ client, gOpts, apiUrl }, opts) => {
|
|
933
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
934
|
+
const data = await client.get(`/agents/${agentId}/funnel/hooks`, {});
|
|
935
|
+
if (gOpts.json) printJson(data);
|
|
936
|
+
else printTable(data.items.map((h) => ({
|
|
937
|
+
Name: h.hook_name,
|
|
938
|
+
Hostname: h.hostname,
|
|
939
|
+
Active: h.active,
|
|
940
|
+
Created: h.created_at
|
|
941
|
+
})));
|
|
942
|
+
}));
|
|
943
|
+
hook.command("delete").description("Delete a funnel hook and purge its local secret").option("--agent <id>", "Agent ID (defaults to your agent)").requiredOption("--name <name>", "Hook name to delete").option("--yes", "Skip confirmation prompt").action(withClient(program, async ({ client, gOpts, apiUrl, configDir }, opts) => {
|
|
944
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
945
|
+
if (!opts.yes && !gOpts.json) {
|
|
946
|
+
if (!await promptConfirm(`Delete funnel hook '${opts.name}'?`)) return;
|
|
947
|
+
}
|
|
948
|
+
try {
|
|
949
|
+
await client.delete(`/agents/${agentId}/funnel/hooks/${opts.name}`);
|
|
950
|
+
} catch (err) {
|
|
951
|
+
if (opts.yes && err instanceof RineApiError && err.status === 404) {
|
|
952
|
+
deleteHookSecret(configDir, agentId, opts.name);
|
|
953
|
+
printMutationOk("Funnel hook already absent", gOpts.json);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
throw err;
|
|
957
|
+
}
|
|
958
|
+
deleteHookSecret(configDir, agentId, opts.name);
|
|
959
|
+
printMutationOk("Funnel hook deleted", gOpts.json);
|
|
960
|
+
}));
|
|
961
|
+
}
|
|
962
|
+
/** Print the print-once GitHub setup block (the secret is shown exactly once). */
|
|
963
|
+
function printCreated(created, name, secret, json) {
|
|
964
|
+
const publicUrl = `https://${created.hostname}/${name}`;
|
|
965
|
+
if (json) {
|
|
966
|
+
printJson({
|
|
967
|
+
hook: {
|
|
968
|
+
name: created.hook_name,
|
|
969
|
+
hostname: created.hostname,
|
|
970
|
+
agent_id: created.agent_id
|
|
971
|
+
},
|
|
972
|
+
public_url: publicUrl,
|
|
973
|
+
secret,
|
|
974
|
+
signature_header: SIGNATURE_HEADER,
|
|
975
|
+
content_type: CONTENT_TYPE
|
|
976
|
+
});
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
printTable([
|
|
980
|
+
{
|
|
981
|
+
Field: "Hook",
|
|
982
|
+
Value: created.hook_name
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
Field: "Hostname",
|
|
986
|
+
Value: created.hostname
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
Field: "Payload URL",
|
|
990
|
+
Value: publicUrl
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
Field: "Content type",
|
|
994
|
+
Value: CONTENT_TYPE
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
Field: "Signature header",
|
|
998
|
+
Value: SIGNATURE_HEADER
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
Field: "Secret",
|
|
1002
|
+
Value: secret
|
|
1003
|
+
}
|
|
1004
|
+
]);
|
|
1005
|
+
console.log(`\nGitHub: Settings → Webhooks → Payload URL = ${publicUrl}, Content type = ${CONTENT_TYPE}, Secret = the secret above.`);
|
|
1006
|
+
console.log("Save this secret now — it is not stored on rine and cannot be retrieved.");
|
|
1007
|
+
}
|
|
1008
|
+
//#endregion
|
|
908
1009
|
//#region src/commands/keys.ts
|
|
909
1010
|
function getKeysDir(configDir, agentId) {
|
|
910
1011
|
return join(configDir, "keys", agentId);
|
|
@@ -1352,68 +1453,6 @@ function registerMessages(program) {
|
|
|
1352
1453
|
addMessageCommands(program.command("message").description("Message operations (aliases: send/read/inbox/reply)"), program);
|
|
1353
1454
|
}
|
|
1354
1455
|
//#endregion
|
|
1355
|
-
//#region src/commands/poll-token.ts
|
|
1356
|
-
function registerPollToken(program) {
|
|
1357
|
-
program.command("poll-token").description("Generate or revoke a poll token for inbox monitoring").option("--agent <id>", "Agent ID (auto-resolved for single-agent orgs)").option("--revoke", "Revoke the poll token").action(withClient(program, async ({ client, gOpts, profileName, configDir, apiUrl }, opts) => {
|
|
1358
|
-
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
1359
|
-
if (opts.revoke) {
|
|
1360
|
-
await client.delete(`/agents/${agentId}/poll-token`);
|
|
1361
|
-
const creds = loadCredentials(configDir);
|
|
1362
|
-
if (creds[profileName]) {
|
|
1363
|
-
delete creds[profileName].poll_url;
|
|
1364
|
-
saveCredentials(configDir, creds);
|
|
1365
|
-
}
|
|
1366
|
-
printMutationOk("Poll token revoked", gOpts.json);
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1369
|
-
const data = await client.post(`/agents/${agentId}/poll-token`, {});
|
|
1370
|
-
const creds = loadCredentials(configDir);
|
|
1371
|
-
if (creds[profileName]) {
|
|
1372
|
-
creds[profileName].poll_url = data.poll_url;
|
|
1373
|
-
saveCredentials(configDir, creds);
|
|
1374
|
-
}
|
|
1375
|
-
if (gOpts.json) printJson(data);
|
|
1376
|
-
else {
|
|
1377
|
-
console.log(`Poll URL: ${data.poll_url}`);
|
|
1378
|
-
console.log("Credentials updated.");
|
|
1379
|
-
}
|
|
1380
|
-
}));
|
|
1381
|
-
}
|
|
1382
|
-
//#endregion
|
|
1383
|
-
//#region src/commands/org.ts
|
|
1384
|
-
function renderOrg(data, opts) {
|
|
1385
|
-
if (opts.json) {
|
|
1386
|
-
printJson(data);
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
printTable([{
|
|
1390
|
-
ID: data.id,
|
|
1391
|
-
Name: data.name,
|
|
1392
|
-
"Contact Email": data.contact_email ?? "",
|
|
1393
|
-
Country: data.country_code ?? "",
|
|
1394
|
-
Created: data.created_at
|
|
1395
|
-
}]);
|
|
1396
|
-
}
|
|
1397
|
-
function registerOrg(program) {
|
|
1398
|
-
const org = program.command("org").description("Organization management");
|
|
1399
|
-
org.command("get").description("Get organization details").action(withClient(program, async ({ client, gOpts }) => {
|
|
1400
|
-
renderOrg(await client.get("/org"), gOpts);
|
|
1401
|
-
}));
|
|
1402
|
-
org.command("update").description("Update organization details").option("--name <name>", "Organization name").option("--contact-email <email>", "Contact email address").option("--country-code <cc>", "ISO country code (e.g. DE)").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1403
|
-
const body = {};
|
|
1404
|
-
if (opts.name) body.name = opts.name;
|
|
1405
|
-
if (opts.contactEmail) body.contact_email = opts.contactEmail;
|
|
1406
|
-
if (opts.countryCode) body.country_code = opts.countryCode;
|
|
1407
|
-
if (Object.keys(body).length === 0) {
|
|
1408
|
-
printError("At least one of --name, --contact-email, --country-code is required");
|
|
1409
|
-
process.exitCode = 2;
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
await client.patch("/org", body);
|
|
1413
|
-
printMutationOk("Org updated", gOpts.json);
|
|
1414
|
-
}));
|
|
1415
|
-
}
|
|
1416
|
-
//#endregion
|
|
1417
1456
|
//#region src/commands/onboard.ts
|
|
1418
1457
|
function registerOnboard(program) {
|
|
1419
1458
|
program.command("onboard").description("Register a new organization and create your first agent").requiredOption("--email <email>", "Email address").requiredOption("--name <name>", "Organization name").requiredOption("--slug <slug>", "Organization slug").requiredOption("--agent <agent-name>", "Name for the first agent").option("--[no-]human-oversight", "Require human oversight (default: true)").option("--unlisted", "Mark agent as unlisted (not in public directory)").action(withErrorHandler(program, async (gOpts, opts) => {
|
|
@@ -1479,6 +1518,68 @@ function registerOnboard(program) {
|
|
|
1479
1518
|
}));
|
|
1480
1519
|
}
|
|
1481
1520
|
//#endregion
|
|
1521
|
+
//#region src/commands/org.ts
|
|
1522
|
+
function renderOrg(data, opts) {
|
|
1523
|
+
if (opts.json) {
|
|
1524
|
+
printJson(data);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
printTable([{
|
|
1528
|
+
ID: data.id,
|
|
1529
|
+
Name: data.name,
|
|
1530
|
+
"Contact Email": data.contact_email ?? "",
|
|
1531
|
+
Country: data.country_code ?? "",
|
|
1532
|
+
Created: data.created_at
|
|
1533
|
+
}]);
|
|
1534
|
+
}
|
|
1535
|
+
function registerOrg(program) {
|
|
1536
|
+
const org = program.command("org").description("Organization management");
|
|
1537
|
+
org.command("get").description("Get organization details").action(withClient(program, async ({ client, gOpts }) => {
|
|
1538
|
+
renderOrg(await client.get("/org"), gOpts);
|
|
1539
|
+
}));
|
|
1540
|
+
org.command("update").description("Update organization details").option("--name <name>", "Organization name").option("--contact-email <email>", "Contact email address").option("--country-code <cc>", "ISO country code (e.g. DE)").action(withClient(program, async ({ client, gOpts }, opts) => {
|
|
1541
|
+
const body = {};
|
|
1542
|
+
if (opts.name) body.name = opts.name;
|
|
1543
|
+
if (opts.contactEmail) body.contact_email = opts.contactEmail;
|
|
1544
|
+
if (opts.countryCode) body.country_code = opts.countryCode;
|
|
1545
|
+
if (Object.keys(body).length === 0) {
|
|
1546
|
+
printError("At least one of --name, --contact-email, --country-code is required");
|
|
1547
|
+
process.exitCode = 2;
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
await client.patch("/org", body);
|
|
1551
|
+
printMutationOk("Org updated", gOpts.json);
|
|
1552
|
+
}));
|
|
1553
|
+
}
|
|
1554
|
+
//#endregion
|
|
1555
|
+
//#region src/commands/poll-token.ts
|
|
1556
|
+
function registerPollToken(program) {
|
|
1557
|
+
program.command("poll-token").description("Generate or revoke a poll token for inbox monitoring").option("--agent <id>", "Agent ID (auto-resolved for single-agent orgs)").option("--revoke", "Revoke the poll token").action(withClient(program, async ({ client, gOpts, profileName, configDir, apiUrl }, opts) => {
|
|
1558
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
1559
|
+
if (opts.revoke) {
|
|
1560
|
+
await client.delete(`/agents/${agentId}/poll-token`);
|
|
1561
|
+
const creds = loadCredentials(configDir);
|
|
1562
|
+
if (creds[profileName]) {
|
|
1563
|
+
delete creds[profileName].poll_url;
|
|
1564
|
+
saveCredentials(configDir, creds);
|
|
1565
|
+
}
|
|
1566
|
+
printMutationOk("Poll token revoked", gOpts.json);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const data = await client.post(`/agents/${agentId}/poll-token`, {});
|
|
1570
|
+
const creds = loadCredentials(configDir);
|
|
1571
|
+
if (creds[profileName]) {
|
|
1572
|
+
creds[profileName].poll_url = data.poll_url;
|
|
1573
|
+
saveCredentials(configDir, creds);
|
|
1574
|
+
}
|
|
1575
|
+
if (gOpts.json) printJson(data);
|
|
1576
|
+
else {
|
|
1577
|
+
console.log(`Poll URL: ${data.poll_url}`);
|
|
1578
|
+
console.log("Credentials updated.");
|
|
1579
|
+
}
|
|
1580
|
+
}));
|
|
1581
|
+
}
|
|
1582
|
+
//#endregion
|
|
1482
1583
|
//#region src/commands/register.ts
|
|
1483
1584
|
function registerRegister(program) {
|
|
1484
1585
|
program.command("register").description("Register a new organization (two-step PoW flow)").requiredOption("--email <email>", "Email address").requiredOption("--name <name>", "Organization name").requiredOption("--slug <slug>", "Organization slug").action(withErrorHandler(program, async (gOpts, opts) => {
|
|
@@ -1504,6 +1605,246 @@ function registerRegister(program) {
|
|
|
1504
1605
|
}));
|
|
1505
1606
|
}
|
|
1506
1607
|
//#endregion
|
|
1608
|
+
//#region src/commands/relay.ts
|
|
1609
|
+
function emitLifecycle$1(json, data) {
|
|
1610
|
+
if (json) console.log(JSON.stringify({
|
|
1611
|
+
event: "lifecycle",
|
|
1612
|
+
data
|
|
1613
|
+
}));
|
|
1614
|
+
}
|
|
1615
|
+
function jitteredDelayMs$1(backoffSeconds) {
|
|
1616
|
+
return Math.round(backoffSeconds * (.5 + Math.random()) * 1e3);
|
|
1617
|
+
}
|
|
1618
|
+
function sleep(ms) {
|
|
1619
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Parse --port into a valid TCP port. `0` is accepted as "OS-assign an ephemeral
|
|
1623
|
+
* port" (it is NOT coerced to the default — that was the L3 footgun); a
|
|
1624
|
+
* non-integer or out-of-range value throws a clear error rather than silently
|
|
1625
|
+
* falling back. The default flag value "8443" parses straight through.
|
|
1626
|
+
*/
|
|
1627
|
+
function parsePort(raw) {
|
|
1628
|
+
const port = Number(raw);
|
|
1629
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`Invalid --port '${raw}': expected an integer 0-65535.`);
|
|
1630
|
+
return port;
|
|
1631
|
+
}
|
|
1632
|
+
function isRevoked(err) {
|
|
1633
|
+
return typeof err === "object" && err !== null && err.revoked === true;
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Resolve the HMAC secret: --secret-env > --secret-file > the persisted file.
|
|
1637
|
+
* Returns "" (and warns, naming the three sources tried) when none is found — an
|
|
1638
|
+
* empty secret can never match GitHub's real signature, so every request drops
|
|
1639
|
+
* safely rather than relaying an unverified body.
|
|
1640
|
+
*/
|
|
1641
|
+
function resolveSecret(opts, configDir, agentId) {
|
|
1642
|
+
if (opts.secretEnv) {
|
|
1643
|
+
const v = process.env[opts.secretEnv];
|
|
1644
|
+
if (v) return v.trim();
|
|
1645
|
+
}
|
|
1646
|
+
if (opts.secretFile) return readFileSync(opts.secretFile, "utf-8").trim();
|
|
1647
|
+
const stored = loadHookSecret(configDir, agentId, opts.hook);
|
|
1648
|
+
if (stored) return stored;
|
|
1649
|
+
process.stderr.write(`No hook secret found (tried --secret-env, --secret-file, and ${configDir}/funnel/${agentId}/${opts.hook}.secret) — all inbound requests will fail verification until one is provided.\n`);
|
|
1650
|
+
return "";
|
|
1651
|
+
}
|
|
1652
|
+
function registerRelay(program) {
|
|
1653
|
+
program.command("relay").description("Run the inbound webhook funnel relay (long-lived foreground daemon)").option("--agent <id>", "Agent ID (defaults to your agent)").option("--hook <name>", "Hook name to relay", "default").option("--port <n>", "Local TLS listener port", "8443").option("--secret-file <path>", "Read the HMAC secret from a file").option("--secret-env <VAR>", "Read the HMAC secret from an env var").option("--staging", "Use the Let's Encrypt staging CA (dev)").option("--verbose", "Verbose reconnect logging").action(async (opts) => {
|
|
1654
|
+
try {
|
|
1655
|
+
const gOpts = program.opts();
|
|
1656
|
+
const configDir = resolveConfigDir();
|
|
1657
|
+
const apiUrl = resolveApiUrl();
|
|
1658
|
+
const { client, profileName, entry } = await createClient(configDir, apiUrl, gOpts.profile);
|
|
1659
|
+
const agentId = await resolveAgent(apiUrl, await fetchAgents(client), opts.agent, gOpts.as);
|
|
1660
|
+
if (!agentKeysExist(configDir, agentId)) {
|
|
1661
|
+
printError(`Agent identity keys not found under ${configDir}/keys/${agentId}/ — register first.`);
|
|
1662
|
+
process.exitCode = 2;
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
await runRelayLoop({
|
|
1666
|
+
opts,
|
|
1667
|
+
gOpts,
|
|
1668
|
+
configDir,
|
|
1669
|
+
apiUrl,
|
|
1670
|
+
agentId,
|
|
1671
|
+
client,
|
|
1672
|
+
profileName,
|
|
1673
|
+
entry
|
|
1674
|
+
});
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1677
|
+
process.exitCode = 1;
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
async function runRelayLoop(ctx) {
|
|
1682
|
+
const { opts, gOpts, configDir, apiUrl, agentId, client, profileName, entry } = ctx;
|
|
1683
|
+
const json = gOpts.json;
|
|
1684
|
+
const port = parsePort(opts.port);
|
|
1685
|
+
const secret = resolveSecret(opts, configDir, agentId);
|
|
1686
|
+
const options = { staging: opts.staging };
|
|
1687
|
+
let backoff = 1;
|
|
1688
|
+
let attempt = 0;
|
|
1689
|
+
let stopped = false;
|
|
1690
|
+
let lastDeps;
|
|
1691
|
+
let certHandle;
|
|
1692
|
+
process.on("SIGINT", () => {
|
|
1693
|
+
stopped = true;
|
|
1694
|
+
process.exitCode = 0;
|
|
1695
|
+
});
|
|
1696
|
+
while (!stopped) {
|
|
1697
|
+
attempt++;
|
|
1698
|
+
let listener;
|
|
1699
|
+
try {
|
|
1700
|
+
const { hostname, control_ws_url: controlWsUrl } = await resolveHook(client, agentId, opts.hook);
|
|
1701
|
+
certHandle = hookLabel(hostname);
|
|
1702
|
+
lastDeps = await buildIssueCertDeps({
|
|
1703
|
+
configDir,
|
|
1704
|
+
options,
|
|
1705
|
+
http: client,
|
|
1706
|
+
agentId
|
|
1707
|
+
});
|
|
1708
|
+
const cert = await ensureCert({
|
|
1709
|
+
handle: certHandle,
|
|
1710
|
+
agentId,
|
|
1711
|
+
delegationId: certHandle,
|
|
1712
|
+
configDir,
|
|
1713
|
+
options,
|
|
1714
|
+
deps: lastDeps
|
|
1715
|
+
});
|
|
1716
|
+
emitLifecycle$1(json, {
|
|
1717
|
+
state: "cert_ready",
|
|
1718
|
+
hostname
|
|
1719
|
+
});
|
|
1720
|
+
const ref = {};
|
|
1721
|
+
const tunnel = await connectTunnel({
|
|
1722
|
+
controlWsUrl,
|
|
1723
|
+
token: await getOrRefreshToken(configDir, apiUrl, entry, profileName, {
|
|
1724
|
+
force: false,
|
|
1725
|
+
envToken: process.env.RINE_TOKEN
|
|
1726
|
+
}),
|
|
1727
|
+
agentId,
|
|
1728
|
+
hostname,
|
|
1729
|
+
openLocalPipe: (frame) => {
|
|
1730
|
+
if (!ref.live) throw new Error("listener not ready");
|
|
1731
|
+
return ref.live.openLocalPipe(frame);
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
emitLifecycle$1(json, { state: "tunnel_connected" });
|
|
1735
|
+
const live = await startTlsListener({
|
|
1736
|
+
port,
|
|
1737
|
+
certPem: cert.certPem,
|
|
1738
|
+
keyPem: cert.keyPem,
|
|
1739
|
+
onRequest: (req) => handleWebhookRequest({
|
|
1740
|
+
configDir,
|
|
1741
|
+
agentId,
|
|
1742
|
+
hookName: opts.hook,
|
|
1743
|
+
secret,
|
|
1744
|
+
client,
|
|
1745
|
+
emitLifecycle: (ev) => json && console.log(JSON.stringify(ev))
|
|
1746
|
+
}, req).then(() => void 0)
|
|
1747
|
+
});
|
|
1748
|
+
listener = live;
|
|
1749
|
+
ref.live = live;
|
|
1750
|
+
emitLifecycle$1(json, {
|
|
1751
|
+
state: "listener_ready",
|
|
1752
|
+
port: live.port
|
|
1753
|
+
});
|
|
1754
|
+
backoff = 1;
|
|
1755
|
+
const close = await waitForClose(tunnel, () => stopped);
|
|
1756
|
+
tunnel.close();
|
|
1757
|
+
live.close();
|
|
1758
|
+
if (stopped) break;
|
|
1759
|
+
if (close?.clean !== false) {
|
|
1760
|
+
emitLifecycle$1(json, {
|
|
1761
|
+
state: "tunnel_reconnecting",
|
|
1762
|
+
attempt: attempt + 1,
|
|
1763
|
+
backoff_ms: 0,
|
|
1764
|
+
reason: "server_close"
|
|
1765
|
+
});
|
|
1766
|
+
backoff = 1;
|
|
1767
|
+
} else {
|
|
1768
|
+
const delay = jitteredDelayMs$1(backoff);
|
|
1769
|
+
emitLifecycle$1(json, {
|
|
1770
|
+
state: "tunnel_reconnecting",
|
|
1771
|
+
attempt: attempt + 1,
|
|
1772
|
+
backoff_ms: delay,
|
|
1773
|
+
reason: "error"
|
|
1774
|
+
});
|
|
1775
|
+
await sleep(delay);
|
|
1776
|
+
backoff = Math.min(backoff * 2, 30);
|
|
1777
|
+
}
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
listener?.close();
|
|
1780
|
+
if (stopped) break;
|
|
1781
|
+
if (isRevoked(err)) {
|
|
1782
|
+
emitLifecycle$1(json, { state: "tunnel_revoked" });
|
|
1783
|
+
if (lastDeps && certHandle) await revokeAndWipeCache({
|
|
1784
|
+
handle: certHandle,
|
|
1785
|
+
configDir,
|
|
1786
|
+
options,
|
|
1787
|
+
deps: lastDeps
|
|
1788
|
+
}).catch(() => {});
|
|
1789
|
+
process.exitCode = 1;
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const delay = jitteredDelayMs$1(backoff);
|
|
1793
|
+
emitLifecycle$1(json, {
|
|
1794
|
+
state: "tunnel_reconnecting",
|
|
1795
|
+
attempt: attempt + 1,
|
|
1796
|
+
backoff_ms: delay,
|
|
1797
|
+
reason: "error"
|
|
1798
|
+
});
|
|
1799
|
+
if (opts.verbose) process.stderr.write(`Reconnecting (attempt ${attempt + 1}, error): ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
1800
|
+
await sleep(delay);
|
|
1801
|
+
backoff = Math.min(backoff * 2, 30);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
emitLifecycle$1(json, {
|
|
1805
|
+
state: "stopped",
|
|
1806
|
+
reason: "signal"
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Resolve when the tunnel WS closes (carrying the classified close so the caller
|
|
1811
|
+
* can pick clean-vs-error reconnect) OR a SIGINT arrives (resolves `undefined`;
|
|
1812
|
+
* the caller's `stopped` flag short-circuits before the reconnect branch).
|
|
1813
|
+
*/
|
|
1814
|
+
function waitForClose(tunnel, isStopped) {
|
|
1815
|
+
return new Promise((resolve) => {
|
|
1816
|
+
if (isStopped()) return resolve(void 0);
|
|
1817
|
+
tunnel.onClose((close) => resolve(close));
|
|
1818
|
+
process.once("SIGINT", () => {
|
|
1819
|
+
tunnel.close();
|
|
1820
|
+
resolve(void 0);
|
|
1821
|
+
});
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* The server hook row — the SINGLE source of truth, used VERBATIM. Its
|
|
1826
|
+
* `hostname` (the `{hook}--{slug}.hook.rine.network` form) goes into the BIND and
|
|
1827
|
+
* (via its label) the cert CN/SAN; its `control_ws_url` is the broker control-WS
|
|
1828
|
+
* endpoint the relay dials. NEITHER is reconstructed client-side: a fabricated
|
|
1829
|
+
* `<uuid>.hook.rine.network` matches no funnel_hook row (BIND would NAK, the cert
|
|
1830
|
+
* would never match GitHub's TLS), and the API base resolves to the primary IP /
|
|
1831
|
+
* Caddy, which has no funnel route. On a missing row or a transient lookup
|
|
1832
|
+
* failure we THROW so the loop backs off and retries.
|
|
1833
|
+
*/
|
|
1834
|
+
async function resolveHook(client, agentId, hookName) {
|
|
1835
|
+
const hook = (await client.get(`/agents/${agentId}/funnel/hooks`, {}))?.items?.find((h) => h.hook_name === hookName);
|
|
1836
|
+
if (!hook?.hostname || !hook.control_ws_url) throw new Error(`No funnel hook '${hookName}' found for agent ${agentId} — run \`rine hook create --name ${hookName}\` first.`);
|
|
1837
|
+
return hook;
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* The DNS LABEL of a hook hostname — everything before `.hook.rine.network`. The
|
|
1841
|
+
* cert is issued for this label via hookFqdn(label), so the leaf's CN/SAN equals
|
|
1842
|
+
* the public hostname GitHub connects to (no name mismatch on the TLS handshake).
|
|
1843
|
+
*/
|
|
1844
|
+
function hookLabel(hostname) {
|
|
1845
|
+
return hostname.endsWith(".hook.rine.network") ? hostname.slice(0, -18) : hostname;
|
|
1846
|
+
}
|
|
1847
|
+
//#endregion
|
|
1507
1848
|
//#region src/commands/stream.ts
|
|
1508
1849
|
function emitLifecycle(gOpts, monitor, data) {
|
|
1509
1850
|
if (gOpts.json) {
|
|
@@ -1789,8 +2130,10 @@ registerMessages(program);
|
|
|
1789
2130
|
registerGroup(program);
|
|
1790
2131
|
registerDiscover(program);
|
|
1791
2132
|
registerWebhook(program);
|
|
2133
|
+
registerHook(program);
|
|
1792
2134
|
registerKeys(program);
|
|
1793
2135
|
registerStream(program);
|
|
2136
|
+
registerRelay(program);
|
|
1794
2137
|
registerPollToken(program);
|
|
1795
2138
|
program.parse();
|
|
1796
2139
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rine-network/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "CLI client for rine.network \u2014 EU-first messaging infrastructure for AI agents",
|
|
5
5
|
"author": "mmmbs <mmmbs@proton.me>",
|
|
6
6
|
"license": "EUPL-1.2",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dev": "tsx src/main.ts"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@rine-network/core": "^0.
|
|
31
|
+
"@rine-network/core": "^0.6.1",
|
|
32
32
|
"commander": "^12.0.0",
|
|
33
33
|
"eventsource-client": "^1.1.0"
|
|
34
34
|
},
|