ai-fly 0.0.0 → 0.1.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/README.md +105 -0
- package/dist/ai-fly.d.ts +1 -0
- package/dist/ai-fly.js +76 -0
- package/dist/common-4VeA_3wo.mjs +347 -0
- package/dist/common-BULbPtYn.mjs +1295 -0
- package/dist/config-CO3WyW2H.mjs +173 -0
- package/dist/errors-BGtxPkM7.mjs +27 -0
- package/dist/forget-BEbNwPip.mjs +22 -0
- package/dist/group-Csy9VmFb.mjs +92 -0
- package/dist/import-BVAFw5Tx.mjs +63 -0
- package/dist/join-B9op0juF.mjs +228 -0
- package/dist/join-E1_-Rt-7.mjs +32 -0
- package/dist/key-Bt4EWiuO.mjs +31 -0
- package/dist/key-CK77IYXp.mjs +67 -0
- package/dist/mux-xHl9Jrbi.mjs +630 -0
- package/dist/ports-BQukx7wS.mjs +46 -0
- package/dist/ports-pI59MtmI.mjs +68 -0
- package/dist/revoke-CyrOzc8t.mjs +34 -0
- package/dist/run-Be8H-IRf.mjs +42 -0
- package/dist/runtime-Dg4en1D8.mjs +1372 -0
- package/dist/sdk-C6xDRbzg.mjs +394 -0
- package/dist/serve-DuyQMeJh.mjs +1665 -0
- package/dist/serve-R03mDBwd.mjs +45 -0
- package/dist/service-Cd4R47n4.mjs +94 -0
- package/dist/share-DGM2TOft.mjs +133 -0
- package/dist/status-DRaKNvqz.mjs +102 -0
- package/dist/status-W2OS7WB6.mjs +57 -0
- package/package.json +64 -6
- package/index.js +0 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { n as UsageError, t as CliError } from "./errors-BGtxPkM7.mjs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
//#region src/cli/args.ts
|
|
8
|
+
function expandTilde(value, homedir) {
|
|
9
|
+
if (value === "~") return homedir;
|
|
10
|
+
if (value.startsWith("~/")) return homedir + value.slice(1);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 解析带 ms|s|m|h|d 后缀的时长;裸数字按毫秒(与 opendweb-example 同规)。
|
|
15
|
+
* 语法错误抛 CliError;值域检查由调用方按各选项边界执行。
|
|
16
|
+
*/
|
|
17
|
+
function parseDurationMs(raw, label) {
|
|
18
|
+
const m = /^([0-9]+(?:\.[0-9]+)?)(ms|s|m|h|d)?$/.exec(String(raw).trim());
|
|
19
|
+
if (!m) throw new CliError(`error: invalid ${label} value: ${raw} (expected <number>[ms|s|m|h|d])`);
|
|
20
|
+
const ms = Number(m[1]) * ({
|
|
21
|
+
ms: 1,
|
|
22
|
+
s: 1e3,
|
|
23
|
+
m: 6e4,
|
|
24
|
+
h: 36e5,
|
|
25
|
+
d: 864e5
|
|
26
|
+
}[m[2] ?? "ms"] ?? 1);
|
|
27
|
+
if (!Number.isFinite(ms) || ms > Number.MAX_SAFE_INTEGER) return Number.POSITIVE_INFINITY;
|
|
28
|
+
return ms;
|
|
29
|
+
}
|
|
30
|
+
function assertDurationRange(ms, minMs, maxMs, label, rangeText) {
|
|
31
|
+
if (!(ms >= minMs && ms <= maxMs)) throw new CliError(`error: ${label} out of range (${rangeText})`);
|
|
32
|
+
}
|
|
33
|
+
function parseArgv(argv, spec, ctx = {}) {
|
|
34
|
+
const homedir = ctx.homedir ?? "";
|
|
35
|
+
const options = {};
|
|
36
|
+
const positionals = [];
|
|
37
|
+
const known = Object.keys(spec);
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const tok = argv[i];
|
|
40
|
+
if (tok === "--") {
|
|
41
|
+
for (let j = i + 1; j < argv.length; j++) positionals.push(argv[j]);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
if (tok.startsWith("--")) {
|
|
45
|
+
const eq = tok.indexOf("=");
|
|
46
|
+
const name = (eq >= 0 ? tok.slice(2, eq) : tok.slice(2)).trim();
|
|
47
|
+
const inlineValue = eq >= 0 ? tok.slice(eq + 1) : void 0;
|
|
48
|
+
const decl = spec[name];
|
|
49
|
+
if (!decl) throw new UsageError(`error: unknown option --${name} (known: ${known.map((k) => `--${k}`).join(", ")})`);
|
|
50
|
+
if (decl.type === "boolean") {
|
|
51
|
+
if (inlineValue !== void 0) throw new UsageError(`error: option --${name} does not take a value`);
|
|
52
|
+
options[name] = true;
|
|
53
|
+
} else {
|
|
54
|
+
let value = inlineValue;
|
|
55
|
+
if (value === void 0) {
|
|
56
|
+
if (i + 1 >= argv.length) throw new UsageError(`error: option --${name} requires a value`);
|
|
57
|
+
value = argv[++i];
|
|
58
|
+
}
|
|
59
|
+
if (decl.tilde) value = expandTilde(value, homedir);
|
|
60
|
+
if (decl.type === "multi") {
|
|
61
|
+
const list = Array.isArray(options[name]) ? options[name] : [];
|
|
62
|
+
list.push(value);
|
|
63
|
+
options[name] = list;
|
|
64
|
+
} else options[name] = value;
|
|
65
|
+
}
|
|
66
|
+
} else positionals.push(tok);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
options,
|
|
70
|
+
positionals
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/wire/z32.ts
|
|
75
|
+
/** z-base-32 字母表(fabric 同款,ybndrfg8…h769)。 */
|
|
76
|
+
const Z32_ALPHABET = "ybndrfg8ejkmcpqxot1uwisza345h769";
|
|
77
|
+
const CHAR_VALUE = (/* @__PURE__ */ new Uint8Array(128)).fill(255);
|
|
78
|
+
for (let i = 0; i < 32; i++) CHAR_VALUE[Z32_ALPHABET.charCodeAt(i)] = i;
|
|
79
|
+
/** 编码:高位在前、5 bit 一组;末组不足 5 bit 左移补零,不输出填充符。 */
|
|
80
|
+
function encodeZ32(bytes) {
|
|
81
|
+
let out = "";
|
|
82
|
+
let acc = 0;
|
|
83
|
+
let bits = 0;
|
|
84
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
85
|
+
acc = acc << 8 | bytes[i];
|
|
86
|
+
bits += 8;
|
|
87
|
+
while (bits >= 5) {
|
|
88
|
+
bits -= 5;
|
|
89
|
+
out += Z32_ALPHABET.charAt(acc >>> bits & 31);
|
|
90
|
+
}
|
|
91
|
+
acc &= (1 << bits) - 1;
|
|
92
|
+
}
|
|
93
|
+
if (bits > 0) out += Z32_ALPHABET.charAt(acc << 5 - bits & 31);
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** 解码:严格规范形校验(非法字符、尾位 padding 非零、非最短长度均抛错)。 */
|
|
97
|
+
function decodeZ32(text) {
|
|
98
|
+
const out = [];
|
|
99
|
+
let acc = 0;
|
|
100
|
+
let bits = 0;
|
|
101
|
+
for (let i = 0; i < text.length; i++) {
|
|
102
|
+
const code = text.charCodeAt(i);
|
|
103
|
+
if (code >= 128 || CHAR_VALUE[code] === 255) throw new Error(`z32: invalid character at index ${i}`);
|
|
104
|
+
acc = acc << 5 | CHAR_VALUE[code];
|
|
105
|
+
bits += 5;
|
|
106
|
+
if (bits >= 8) {
|
|
107
|
+
bits -= 8;
|
|
108
|
+
out.push(acc >>> bits & 255);
|
|
109
|
+
acc &= (1 << bits) - 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (bits > 0 && acc !== 0) throw new Error("z32: non-canonical trailing bits");
|
|
113
|
+
const bytes = Uint8Array.from(out);
|
|
114
|
+
if (encodeZ32(bytes).length !== text.length) throw new Error("z32: non-canonical length");
|
|
115
|
+
return bytes;
|
|
116
|
+
}
|
|
117
|
+
/** nBytes 字节密码学随机数的 z32 编码(request-id / serviceId / 密钥体共用)。 */
|
|
118
|
+
function randomZ32(nBytes) {
|
|
119
|
+
return encodeZ32(randomBytes(nBytes));
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/cli/config.ts
|
|
123
|
+
const configFileSchema = z.object({
|
|
124
|
+
/** relay 入口 URL 列表(自托管 gateway 地址,如 http://192.168.2.13:8787) */
|
|
125
|
+
relayUrls: z.array(z.string().url()).optional() });
|
|
126
|
+
function configDir(base = homedir()) {
|
|
127
|
+
return join(base, ".aifly");
|
|
128
|
+
}
|
|
129
|
+
function configPath(base = homedir()) {
|
|
130
|
+
return join(configDir(base), "config.json");
|
|
131
|
+
}
|
|
132
|
+
/** 读取并校验配置文件;不存在返回 {};损坏/非法时抛 CliError(不静默重置)。 */
|
|
133
|
+
function loadConfig(base = homedir()) {
|
|
134
|
+
const p = configPath(base);
|
|
135
|
+
if (!existsSync(p)) return {};
|
|
136
|
+
let raw;
|
|
137
|
+
try {
|
|
138
|
+
raw = readFileSync(p, "utf8");
|
|
139
|
+
} catch (err) {
|
|
140
|
+
throw new CliError(`error: cannot read config ${p}: ${err.message}`);
|
|
141
|
+
}
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(raw);
|
|
145
|
+
} catch {
|
|
146
|
+
throw new CliError(`error: config ${p} is not valid JSON (fix or remove it manually)`);
|
|
147
|
+
}
|
|
148
|
+
const result = configFileSchema.safeParse(parsed);
|
|
149
|
+
if (!result.success) throw new CliError(`error: config ${p} failed validation: ${result.error.message}`);
|
|
150
|
+
return result.data;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 解析生效的 relay 入口:flag > 链接内嵌(import 消费侧)> env(AIFLY_RELAY,逗号
|
|
154
|
+
* 分隔) > config file。返回 undefined 表示走 SDK 默认(n0 公共 relay)。
|
|
155
|
+
* 链接层级的语义:aifly1. 链接内嵌的 relayUrls 是提供方签发 invite 的入口——
|
|
156
|
+
* 兑换与后续连接都必须发生在同一 relay 网,故高于机器级 env/config(2026-09-09
|
|
157
|
+
* 实机踩坑:不带 --relay 时落 SDK 公网默认,兑换成功但连接永败且无提示)。
|
|
158
|
+
*/
|
|
159
|
+
function resolveRelayUrls(input) {
|
|
160
|
+
const fromFlag = input.flag?.filter((u) => u.length > 0);
|
|
161
|
+
if (fromFlag && fromFlag.length > 0) return [...fromFlag];
|
|
162
|
+
const fromLink = input.link?.filter((u) => u.length > 0);
|
|
163
|
+
if (fromLink && fromLink.length > 0) return [...fromLink];
|
|
164
|
+
const envRaw = input.env?.trim();
|
|
165
|
+
if (envRaw) {
|
|
166
|
+
const urls = envRaw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
167
|
+
if (urls.length > 0) return urls;
|
|
168
|
+
}
|
|
169
|
+
const fromFile = input.file?.relayUrls;
|
|
170
|
+
if (fromFile && fromFile.length > 0) return [...fromFile];
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
export { assertDurationRange as a, randomZ32 as i, resolveRelayUrls as n, parseArgv as o, decodeZ32 as r, parseDurationMs as s, loadConfig as t };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/cli/errors.ts
|
|
2
|
+
var CliError = class extends Error {
|
|
3
|
+
exitCode;
|
|
4
|
+
constructor(message, exitCode = 1) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "CliError";
|
|
7
|
+
this.exitCode = exitCode;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var UsageError = class extends CliError {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message, 2);
|
|
13
|
+
this.name = "UsageError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** 顶层兜底:把 Error 映射为终端输出 + 退出码;CliError 按其携带码,其余按 1。 */
|
|
17
|
+
function reportCliError(err) {
|
|
18
|
+
if (err instanceof CliError) {
|
|
19
|
+
process.stderr.write(`${err.message}\n`);
|
|
20
|
+
return err.exitCode;
|
|
21
|
+
}
|
|
22
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23
|
+
process.stderr.write(`error: ${msg}\n`);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { UsageError as n, reportCliError as r, CliError as t };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { n as UsageError } from "./errors-BGtxPkM7.mjs";
|
|
2
|
+
import { o as parseArgv } from "./config-CO3WyW2H.mjs";
|
|
3
|
+
import { _ as removeKeyring, c as consumersRoot, n as ctxHomedir, r as ctxOut } from "./common-4VeA_3wo.mjs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
//#region src/cli/commands/consumer/forget.ts
|
|
6
|
+
async function run(argv, ctx = {}) {
|
|
7
|
+
const { options, positionals } = parseArgv(argv, { data: {
|
|
8
|
+
type: "string",
|
|
9
|
+
tilde: true
|
|
10
|
+
} }, { homedir: ctx.homedir ?? homedir() });
|
|
11
|
+
const ref = positionals[0];
|
|
12
|
+
if (ref === void 0) throw new UsageError("error: forget requires a provider reference\nusage: ai-fly forget <endpointId|8-char-prefix|alias> [--data <dir>]");
|
|
13
|
+
const out = ctxOut(ctx);
|
|
14
|
+
const root = consumersRoot(options.data, ctxHomedir(ctx));
|
|
15
|
+
const removed = removeKeyring(root, ref);
|
|
16
|
+
out(`forgot provider '${removed.ring.alias}' (${removed.ring.endpointId})`);
|
|
17
|
+
out(`removed ${removed.dir} (keyring + fabric identity)`);
|
|
18
|
+
out("note: provider-side revocation must be done on the provider (ai-fly key revoke / ai-fly revoke)");
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { run };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { n as UsageError, r as reportCliError } from "./errors-BGtxPkM7.mjs";
|
|
2
|
+
import { o as parseArgv } from "./config-CO3WyW2H.mjs";
|
|
3
|
+
import { d as str, l as resolveDataDir, n as multi, r as openStore, s as parsePositiveInt } from "./common-BULbPtYn.mjs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
//#region src/cli/commands/provider/group.ts
|
|
6
|
+
const SPEC = {
|
|
7
|
+
data: {
|
|
8
|
+
type: "string",
|
|
9
|
+
tilde: true
|
|
10
|
+
},
|
|
11
|
+
service: { type: "multi" },
|
|
12
|
+
"max-concurrency": { type: "string" },
|
|
13
|
+
"daily-requests": { type: "string" }
|
|
14
|
+
};
|
|
15
|
+
const USAGE = `usage:
|
|
16
|
+
ai-fly group add <name> [--service <serviceName>]... [--max-concurrency <n>] [--daily-requests <n>] [--data <dir>]
|
|
17
|
+
ai-fly group set-services <name> [--service <serviceName>]... [--data <dir>]
|
|
18
|
+
ai-fly group remove <name> [--data <dir>]
|
|
19
|
+
ai-fly group list [--data <dir>]`;
|
|
20
|
+
async function run(argv, ctx = {}) {
|
|
21
|
+
try {
|
|
22
|
+
const home = ctx.homedir ?? homedir();
|
|
23
|
+
const { options, positionals } = parseArgv(argv, SPEC, { homedir: home });
|
|
24
|
+
const sub = positionals[0];
|
|
25
|
+
if (sub === "add") return add(options, positionals, home);
|
|
26
|
+
if (sub === "set-services") return setServices(options, positionals, home);
|
|
27
|
+
if (sub === "remove") return remove(options, positionals, home);
|
|
28
|
+
if (sub === "list") return list(options, home);
|
|
29
|
+
if (sub === void 0) throw new UsageError(USAGE);
|
|
30
|
+
throw new UsageError(`error: unknown group subcommand '${sub}' (known: add, set-services, remove, list)`);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return reportCliError(err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function add(options, positionals, home) {
|
|
36
|
+
const name = positionals[1];
|
|
37
|
+
if (name === void 0) throw new UsageError("error: group add requires a <name> argument");
|
|
38
|
+
const services = multi(options.service);
|
|
39
|
+
const maxConcurrency = options["max-concurrency"] === void 0 ? void 0 : parsePositiveInt(str(options["max-concurrency"]), "max-concurrency");
|
|
40
|
+
const dailyRequests = options["daily-requests"] === void 0 ? void 0 : parsePositiveInt(str(options["daily-requests"]), "daily-requests");
|
|
41
|
+
const group = openStore(resolveDataDir(str(options.data), home)).addGroup(name, services, maxConcurrency === void 0 && dailyRequests === void 0 ? void 0 : {
|
|
42
|
+
maxConcurrency,
|
|
43
|
+
dailyRequests
|
|
44
|
+
});
|
|
45
|
+
const limits = group.limits === void 0 ? "unlimited" : `maxConcurrency=${group.limits.maxConcurrency ?? "-"}, dailyRequests=${group.limits.dailyRequests ?? "-"}`;
|
|
46
|
+
process.stdout.write([
|
|
47
|
+
`group added: ${group.name}`,
|
|
48
|
+
` services: ${services.length > 0 ? services.join(", ") : "(none yet)"}`,
|
|
49
|
+
` limits : ${limits}`,
|
|
50
|
+
""
|
|
51
|
+
].join("\n"));
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
function setServices(options, positionals, home) {
|
|
55
|
+
const name = positionals[1];
|
|
56
|
+
if (name === void 0) throw new UsageError("error: group set-services requires a <name> argument");
|
|
57
|
+
const services = multi(options.service);
|
|
58
|
+
if (services.length === 0) throw new UsageError("error: group set-services requires at least one --service <serviceName> (use the store API to empty a group)");
|
|
59
|
+
const store = openStore(resolveDataDir(str(options.data), home));
|
|
60
|
+
const group = store.setGroupServices(name, services);
|
|
61
|
+
const names = group.serviceIds.map((id) => store.getService(id)?.name ?? id).join(", ");
|
|
62
|
+
process.stdout.write(`group updated: ${group.name}\n services: ${names}\n`);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
function remove(options, positionals, home) {
|
|
66
|
+
const name = positionals[1];
|
|
67
|
+
if (name === void 0) throw new UsageError("error: group remove requires a <name> argument");
|
|
68
|
+
openStore(resolveDataDir(str(options.data), home)).removeGroup(name);
|
|
69
|
+
process.stdout.write(`group removed: ${name}\n`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
function list(options, home) {
|
|
73
|
+
const store = openStore(resolveDataDir(str(options.data), home));
|
|
74
|
+
const groups = store.listGroups();
|
|
75
|
+
if (groups.length === 0) {
|
|
76
|
+
process.stdout.write("no groups configured (see: ai-fly group add)\n");
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
const serviceNames = new Map(store.listServices().map((s) => [s.serviceId, s.name]));
|
|
80
|
+
const keys = store.listKeys();
|
|
81
|
+
const lines = [];
|
|
82
|
+
for (const g of groups) {
|
|
83
|
+
const members = g.serviceIds.map((id) => serviceNames.get(id) ?? id).join(", ");
|
|
84
|
+
const limits = g.limits === void 0 ? "unlimited" : `maxConcurrency=${g.limits.maxConcurrency ?? "-"}, dailyRequests=${g.limits.dailyRequests ?? "-"}`;
|
|
85
|
+
const activeKeys = keys.filter((k) => k.group === g.name && k.revokedAt === void 0).length;
|
|
86
|
+
lines.push(`${g.name} (services: ${members || "(none)"}; keys active: ${activeKeys}; limits: ${limits})`);
|
|
87
|
+
}
|
|
88
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { run };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { n as UsageError } from "./errors-BGtxPkM7.mjs";
|
|
2
|
+
import { o as parseArgv } from "./config-CO3WyW2H.mjs";
|
|
3
|
+
import { a as ringsForRun, c as consumersRoot, i as printListeners, n as ctxHomedir, o as waitForSignals, p as listKeyrings, r as ctxOut, t as createSdkFabricFactory } from "./common-4VeA_3wo.mjs";
|
|
4
|
+
import { a as importLink, i as formatLinkPreview, n as decodeShareLink, r as formatKeyringSummary } from "./join-B9op0juF.mjs";
|
|
5
|
+
import { n as createFabricProviderTransport, t as startEngine } from "./runtime-Dg4en1D8.mjs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
//#region src/cli/commands/consumer/import.ts
|
|
8
|
+
async function run(argv, ctx = {}) {
|
|
9
|
+
const { options, positionals } = parseArgv(argv, {
|
|
10
|
+
data: {
|
|
11
|
+
type: "string",
|
|
12
|
+
tilde: true
|
|
13
|
+
},
|
|
14
|
+
preview: { type: "boolean" },
|
|
15
|
+
run: { type: "boolean" },
|
|
16
|
+
"strict-ports": { type: "boolean" },
|
|
17
|
+
relay: { type: "multi" }
|
|
18
|
+
}, { homedir: ctx.homedir ?? homedir() });
|
|
19
|
+
const link = positionals[0];
|
|
20
|
+
if (link === void 0) throw new UsageError("error: import requires a share link argument\nusage: ai-fly import <aifly1-link> [--data <dir>] [--preview] [--run]");
|
|
21
|
+
const out = ctxOut(ctx);
|
|
22
|
+
const root = consumersRoot(options.data, ctxHomedir(ctx));
|
|
23
|
+
const payload = decodeShareLink(link);
|
|
24
|
+
if (options.preview === true) {
|
|
25
|
+
for (const line of formatLinkPreview(payload)) out(line);
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
const linkRelays = payload.provider.relayUrls;
|
|
29
|
+
const flagRelays = options.relay;
|
|
30
|
+
if (flagRelays !== void 0 && flagRelays.length > 0 && linkRelays.length > 0) {
|
|
31
|
+
if (!(flagRelays.length === linkRelays.length && flagRelays.every((u) => linkRelays.includes(u)))) out("warning: --relay overrides the share link's relay entries; if the provider stays unreachable, drop the flag to use the link's entries");
|
|
32
|
+
} else if (linkRelays.length > 0) out(`relay: using ${linkRelays.length} entry URL(s) from the share link (override with --relay)`);
|
|
33
|
+
const factory = await createSdkFabricFactory(flagRelays, ctx, linkRelays);
|
|
34
|
+
const result = await importLink(link, {
|
|
35
|
+
consumersRoot: root,
|
|
36
|
+
fabric: factory
|
|
37
|
+
});
|
|
38
|
+
out(result.redeemed ? "invite redeemed - fabric identity created" : "existing fabric identity reused - invite not consumed");
|
|
39
|
+
for (const line of formatKeyringSummary(result.ring)) out(line);
|
|
40
|
+
if (options.run === true) {
|
|
41
|
+
const { rings, warnings } = listKeyrings(root);
|
|
42
|
+
for (const w of warnings) out(`warning: ${w}`);
|
|
43
|
+
const engine = await startEngine({
|
|
44
|
+
rings: ringsForRun(rings),
|
|
45
|
+
consumersRoot: root,
|
|
46
|
+
sessionFactoryFor: (ring) => createFabricProviderTransport(factory, {
|
|
47
|
+
dataDir: `${root}/${ring.endpointId.slice(0, 8)}/fabric`,
|
|
48
|
+
providerEndpointId: ring.endpointId,
|
|
49
|
+
...ring.relayUrls.length > 0 ? { relayUrls: ring.relayUrls } : {}
|
|
50
|
+
}),
|
|
51
|
+
strictPorts: options["strict-ports"] === true,
|
|
52
|
+
onNotice: out
|
|
53
|
+
});
|
|
54
|
+
printListeners(out, engine.gateway.listenerInfo());
|
|
55
|
+
out("gateway running - press Ctrl-C to stop");
|
|
56
|
+
await waitForSignals();
|
|
57
|
+
await engine.stop();
|
|
58
|
+
out("gateway stopped");
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { run };
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { t as CliError } from "./errors-BGtxPkM7.mjs";
|
|
2
|
+
import { i as randomZ32, r as decodeZ32 } from "./config-CO3WyW2H.mjs";
|
|
3
|
+
import { u as SERVICE_ENTRY_SCHEMA } from "./sdk-C6xDRbzg.mjs";
|
|
4
|
+
import { b as upsertKey, d as hasFabricIdentity, f as keyringDir, h as mergeImportView, l as fabricDir, m as loadKeyring, u as findKeyringDir, v as saveKeyring } from "./common-4VeA_3wo.mjs";
|
|
5
|
+
import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
//#region src/consumer/join.ts
|
|
9
|
+
const SHARE_LINK_PREFIX = "aifly1.";
|
|
10
|
+
const KEY_PREFIX = "sk-aifly-";
|
|
11
|
+
const INVITE_PREFIX = "dweb1.";
|
|
12
|
+
/** base64url 字符集(解码前先做形态校验,拒绝 Buffer 宽容解析)。 */
|
|
13
|
+
const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
14
|
+
const LINK_PAYLOAD_SCHEMA = z.strictObject({
|
|
15
|
+
v: z.literal(1),
|
|
16
|
+
invite: z.string().refine((s) => s.startsWith("dweb1.") && s.length > 6, { message: `invite must be a '${INVITE_PREFIX}' token` }),
|
|
17
|
+
key: z.string().refine((s) => s.startsWith("sk-aifly-") && s.length > 9, { message: `key must be a '${KEY_PREFIX}' secret` }),
|
|
18
|
+
keyId: z.string().min(1).max(128),
|
|
19
|
+
provider: z.strictObject({
|
|
20
|
+
alias: z.string().min(1).max(256),
|
|
21
|
+
endpointId: z.string().min(8).max(128),
|
|
22
|
+
relayUrls: z.array(z.string().min(1).max(2048))
|
|
23
|
+
}),
|
|
24
|
+
group: z.string().min(1).max(256),
|
|
25
|
+
services: z.array(SERVICE_ENTRY_SCHEMA).max(256)
|
|
26
|
+
});
|
|
27
|
+
/** 解码 aifly1.<base64url(payload JSON)>;任何坏形态以 CliError(退出码 1)报出。 */
|
|
28
|
+
function decodeShareLink(link) {
|
|
29
|
+
if (!link.startsWith("aifly1.")) throw new CliError(`error: not an ai-fly share link (expected '${SHARE_LINK_PREFIX}<base64url>)`);
|
|
30
|
+
const body = link.slice(7);
|
|
31
|
+
if (body.length === 0 || !BASE64URL_RE.test(body)) throw new CliError("error: share link payload is not valid base64url");
|
|
32
|
+
let json;
|
|
33
|
+
try {
|
|
34
|
+
json = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
35
|
+
} catch {
|
|
36
|
+
throw new CliError("error: share link payload is not valid JSON");
|
|
37
|
+
}
|
|
38
|
+
const parsed = LINK_PAYLOAD_SCHEMA.safeParse(json);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
const first = parsed.error.issues[0];
|
|
41
|
+
const where = first?.path.length ? ` at ${first.path.join(".")}` : "";
|
|
42
|
+
throw new CliError(`error: share link payload failed validation${where}: ${first?.message ?? "unknown"}`);
|
|
43
|
+
}
|
|
44
|
+
return parsed.data;
|
|
45
|
+
}
|
|
46
|
+
/** --preview 摘要(纯文本、英文 ASCII、零网络)。 */
|
|
47
|
+
function formatLinkPreview(payload) {
|
|
48
|
+
const lines = [];
|
|
49
|
+
lines.push(`provider : ${payload.provider.alias} (${payload.provider.endpointId})`);
|
|
50
|
+
lines.push(`group : ${payload.group}`);
|
|
51
|
+
lines.push(`key id : ${payload.keyId}`);
|
|
52
|
+
lines.push(`relay : ${payload.provider.relayUrls.length > 0 ? payload.provider.relayUrls.join(", ") : "(none)"}`);
|
|
53
|
+
lines.push(`services :`);
|
|
54
|
+
for (const s of payload.services) {
|
|
55
|
+
const match = s.match.map((m) => `${m.type}:${m.value}`).join("|");
|
|
56
|
+
lines.push(` - ${s.name} [${s.serviceId}] default port ${s.defaultPort}${match ? ` (${match})` : ""}`);
|
|
57
|
+
}
|
|
58
|
+
lines.push("note: this link embeds a secret key - treat it like a password");
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 设备入网:staging 目录先行兑换(失败即弃),成功后从名册识别提供者(root:
|
|
63
|
+
* 非自身成员中 sinceMs 最早者),把 fabric 身份目录归位到 <endpointId8>/fabric 并
|
|
64
|
+
* 写入空钥环骨架。既有身份则丢弃 staging 并提示(令牌已被消耗属 fabric 语义)。
|
|
65
|
+
*/
|
|
66
|
+
async function joinDevice(token, root, deps) {
|
|
67
|
+
if (!token.startsWith("dweb1.")) throw new CliError(`error: not a fabric invite token (expected '${INVITE_PREFIX}...')`);
|
|
68
|
+
const staging = join(root, `.join-staging-${randomZ32(4)}`);
|
|
69
|
+
let fabric;
|
|
70
|
+
try {
|
|
71
|
+
mkdirSync(staging, { recursive: true });
|
|
72
|
+
fabric = await deps.fabric.joinWithToken({ dataDir: staging }, token);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
rmSync(staging, {
|
|
75
|
+
recursive: true,
|
|
76
|
+
force: true
|
|
77
|
+
});
|
|
78
|
+
throw new CliError(`error: join failed: ${err.message}`);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const me = fabric.endpointId;
|
|
82
|
+
const provider = (await fabric.members()).filter((m) => m.endpointId !== me).sort((a, b) => a.sinceMs - b.sinceMs)[0];
|
|
83
|
+
if (provider === void 0) throw new CliError("error: joined fabric has no provider member (nothing to consume)");
|
|
84
|
+
const target = keyringDir(root, provider.endpointId);
|
|
85
|
+
if (hasFabricIdentity(root, provider.endpointId)) {
|
|
86
|
+
const existing = loadKeyring(root, provider.endpointId);
|
|
87
|
+
const alias = existing?.alias ?? provider.displayName ?? provider.endpointId.slice(0, 8);
|
|
88
|
+
const ring = existing ?? {
|
|
89
|
+
alias,
|
|
90
|
+
endpointId: provider.endpointId,
|
|
91
|
+
relayUrls: [],
|
|
92
|
+
keys: [],
|
|
93
|
+
services: [],
|
|
94
|
+
ports: {}
|
|
95
|
+
};
|
|
96
|
+
if (existing === void 0) saveKeyring(root, ring);
|
|
97
|
+
return {
|
|
98
|
+
ring,
|
|
99
|
+
provider,
|
|
100
|
+
alreadyJoined: true
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
mkdirSync(keyringDir(root, provider.endpointId), { recursive: true });
|
|
104
|
+
rmSync(join(target, "fabric"), {
|
|
105
|
+
recursive: true,
|
|
106
|
+
force: true
|
|
107
|
+
});
|
|
108
|
+
renameSync(staging, fabricDir(root, provider.endpointId));
|
|
109
|
+
const ring = {
|
|
110
|
+
alias: provider.displayName && provider.displayName.length > 0 ? provider.displayName : provider.endpointId.slice(0, 8),
|
|
111
|
+
endpointId: provider.endpointId,
|
|
112
|
+
relayUrls: [],
|
|
113
|
+
keys: [],
|
|
114
|
+
services: [],
|
|
115
|
+
ports: {}
|
|
116
|
+
};
|
|
117
|
+
const existing = loadKeyring(root, provider.endpointId);
|
|
118
|
+
saveKeyring(root, existing ?? ring);
|
|
119
|
+
return {
|
|
120
|
+
ring: existing ?? ring,
|
|
121
|
+
provider,
|
|
122
|
+
alreadyJoined: false
|
|
123
|
+
};
|
|
124
|
+
} finally {
|
|
125
|
+
await fabric.shutdown().catch(() => void 0);
|
|
126
|
+
rmSync(staging, {
|
|
127
|
+
recursive: true,
|
|
128
|
+
force: true
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** 裸密钥格式校验:sk-aifly-<z32(32B)>(z32 严格解码拒绝非规范形)。 */
|
|
133
|
+
function assertKeyFormat(key) {
|
|
134
|
+
if (!key.startsWith("sk-aifly-")) throw new CliError(`error: not an ai-fly key (expected '${KEY_PREFIX}<z32>)`);
|
|
135
|
+
const body = key.slice(9);
|
|
136
|
+
try {
|
|
137
|
+
const bytes = decodeZ32(body);
|
|
138
|
+
if (bytes.length !== 32) throw new Error(`expected 32 bytes, got ${bytes.length}`);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
throw new CliError(`error: invalid key format: ${err.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 裸密钥入环:定位已入网提供者(endpointId / 8 字符前缀 / 别名;要求已有 fabric
|
|
145
|
+
* 身份或既有钥环),未找到报错并指引先 join/import。keyId/group 由下次 AUTH_OK 回填。
|
|
146
|
+
*/
|
|
147
|
+
function addKey(key, providerRef, root) {
|
|
148
|
+
assertKeyFormat(key);
|
|
149
|
+
const dir = findKeyringDir(root, providerRef);
|
|
150
|
+
if (dir === void 0) throw new CliError(`error: provider '${providerRef}' is not joined on this machine - run 'ai-fly join <invite>' or 'ai-fly import <link>' first (a bare key carries no network info)`);
|
|
151
|
+
const ring = loadKeyring(root, providerRef);
|
|
152
|
+
if (ring === void 0) throw new CliError(`error: provider '${providerRef}' has no readable keyring at ${dir}`);
|
|
153
|
+
if (!hasFabricIdentity(root, ring.endpointId)) throw new CliError(`error: provider '${ring.alias}' has keys but no fabric identity - run 'ai-fly join <invite>' first`);
|
|
154
|
+
const result = upsertKey(ring, {
|
|
155
|
+
keyId: "",
|
|
156
|
+
key,
|
|
157
|
+
group: ""
|
|
158
|
+
});
|
|
159
|
+
saveKeyring(root, result.ring);
|
|
160
|
+
return {
|
|
161
|
+
ring: result.ring,
|
|
162
|
+
added: result.added
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 组合链接导入:本机已有该 endpointId 的 fabric 目录 → Fabric.open 校验既有身份、
|
|
167
|
+
* 跳过兑换;否则 Fabric.joinWithToken 兑换(失败整体回收新目录)。两种路径统一
|
|
168
|
+
* 将链接内 {key,keyId,group} 入环、链接服务视图并入后持久化。
|
|
169
|
+
*/
|
|
170
|
+
async function importLink(link, opts) {
|
|
171
|
+
const payload = decodeShareLink(link);
|
|
172
|
+
const root = opts.consumersRoot;
|
|
173
|
+
const target = keyringDir(root, payload.provider.endpointId);
|
|
174
|
+
const fabricPath = fabricDir(root, payload.provider.endpointId);
|
|
175
|
+
let redeemed = false;
|
|
176
|
+
if (hasFabricIdentity(root, payload.provider.endpointId)) try {
|
|
177
|
+
await (await opts.fabric.open({ dataDir: fabricPath })).shutdown().catch(() => void 0);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
throw new CliError(`error: existing fabric identity for '${payload.provider.alias}' failed to load: ${err.message}`);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
redeemed = true;
|
|
183
|
+
const created = !existsSync(target);
|
|
184
|
+
try {
|
|
185
|
+
mkdirSync(target, { recursive: true });
|
|
186
|
+
await (await opts.fabric.joinWithToken({ dataDir: fabricPath }, payload.invite)).shutdown().catch(() => void 0);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (created) rmSync(target, {
|
|
189
|
+
recursive: true,
|
|
190
|
+
force: true
|
|
191
|
+
});
|
|
192
|
+
else rmSync(fabricPath, {
|
|
193
|
+
recursive: true,
|
|
194
|
+
force: true
|
|
195
|
+
});
|
|
196
|
+
throw new CliError(`error: import failed: ${err.message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const existing = loadKeyring(root, payload.provider.endpointId);
|
|
200
|
+
const { ring } = mergeImportView(existing, {
|
|
201
|
+
...payload.provider,
|
|
202
|
+
services: payload.services
|
|
203
|
+
}, {
|
|
204
|
+
keyId: payload.keyId,
|
|
205
|
+
key: payload.key,
|
|
206
|
+
group: payload.group
|
|
207
|
+
});
|
|
208
|
+
saveKeyring(root, ring);
|
|
209
|
+
return {
|
|
210
|
+
ring,
|
|
211
|
+
payload,
|
|
212
|
+
redeemed
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** 导入/入环后打印的钥环摘要行(别名/EndpointId/relay/分组服务与默认端口)。 */
|
|
216
|
+
function formatKeyringSummary(ring, extra) {
|
|
217
|
+
const lines = [];
|
|
218
|
+
lines.push(`provider : ${ring.alias} (${ring.endpointId})`);
|
|
219
|
+
lines.push(`relay : ${ring.relayUrls.length > 0 ? ring.relayUrls.join(", ") : "(none)"}`);
|
|
220
|
+
lines.push(`keyring : ${ring.keys.length} key(s)${ring.keys.length > 0 ? ` - ${[...new Set(ring.keys.map((k) => k.group).filter((g) => g !== ""))].join(", ")}` : ""}`);
|
|
221
|
+
lines.push(`services :`);
|
|
222
|
+
if (ring.services.length === 0) lines.push(` (none yet - run 'ai-fly run' to connect and fetch the catalog)`);
|
|
223
|
+
for (const s of ring.services) lines.push(` - ${s.name} [${s.serviceId}] default port ${s.defaultPort}`);
|
|
224
|
+
if (extra !== void 0) lines.push(...extra);
|
|
225
|
+
return lines;
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
export { importLink as a, formatLinkPreview as i, decodeShareLink as n, joinDevice as o, formatKeyringSummary as r, addKey as t };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { n as UsageError } from "./errors-BGtxPkM7.mjs";
|
|
2
|
+
import { o as parseArgv } from "./config-CO3WyW2H.mjs";
|
|
3
|
+
import { c as consumersRoot, n as ctxHomedir, r as ctxOut, t as createSdkFabricFactory } from "./common-4VeA_3wo.mjs";
|
|
4
|
+
import { o as joinDevice } from "./join-B9op0juF.mjs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
//#region src/cli/commands/consumer/join.ts
|
|
7
|
+
async function run(argv, ctx = {}) {
|
|
8
|
+
const { options, positionals } = parseArgv(argv, {
|
|
9
|
+
data: {
|
|
10
|
+
type: "string",
|
|
11
|
+
tilde: true
|
|
12
|
+
},
|
|
13
|
+
relay: { type: "multi" }
|
|
14
|
+
}, { homedir: ctx.homedir ?? homedir() });
|
|
15
|
+
const token = positionals[0];
|
|
16
|
+
if (token === void 0) throw new UsageError("error: join requires an invite token argument\nusage: ai-fly join <dweb1-token> [--data <dir>]");
|
|
17
|
+
const out = ctxOut(ctx);
|
|
18
|
+
const root = consumersRoot(options.data, ctxHomedir(ctx));
|
|
19
|
+
const factory = await createSdkFabricFactory(options.relay, ctx);
|
|
20
|
+
const result = await joinDevice(token, root, { fabric: factory });
|
|
21
|
+
if (result.alreadyJoined) {
|
|
22
|
+
out(`already joined to provider '${result.ring.alias}' (${result.ring.endpointId})`);
|
|
23
|
+
out("note: the invite token was consumed by this redemption; the existing identity was kept");
|
|
24
|
+
} else {
|
|
25
|
+
out(`joined to provider '${result.ring.alias}' (${result.ring.endpointId})`);
|
|
26
|
+
out(`fabric identity: ${root}/${result.ring.endpointId.slice(0, 8)}/fabric`);
|
|
27
|
+
out("no keys in the keyring yet - get a share link (ai-fly import <aifly1.>) or a bare key (ai-fly key add)");
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { run };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { n as UsageError } from "./errors-BGtxPkM7.mjs";
|
|
2
|
+
import { o as parseArgv } from "./config-CO3WyW2H.mjs";
|
|
3
|
+
import { c as consumersRoot, n as ctxHomedir, r as ctxOut } from "./common-4VeA_3wo.mjs";
|
|
4
|
+
import { r as formatKeyringSummary, t as addKey } from "./join-B9op0juF.mjs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
//#region src/cli/commands/consumer/key.ts
|
|
7
|
+
async function run(argv, ctx = {}) {
|
|
8
|
+
const sub = argv[0];
|
|
9
|
+
if (sub === void 0) throw new UsageError("error: key requires a subcommand\nusage (consumer): ai-fly key add <sk-aifly-key> --provider <id|alias>\nusage (provider): ai-fly key issue|list|revoke --group <name>");
|
|
10
|
+
if (sub !== "add") throw new UsageError(`error: 'key ${sub}' is a provider-side subcommand (issue/list/revoke run on the provider machine)\nusage (consumer): ai-fly key add <sk-aifly-key> --provider <id|alias>`);
|
|
11
|
+
const { options, positionals } = parseArgv(argv.slice(1), {
|
|
12
|
+
data: {
|
|
13
|
+
type: "string",
|
|
14
|
+
tilde: true
|
|
15
|
+
},
|
|
16
|
+
provider: { type: "string" }
|
|
17
|
+
}, { homedir: ctx.homedir ?? homedir() });
|
|
18
|
+
const key = positionals[0];
|
|
19
|
+
if (key === void 0) throw new UsageError("error: key add requires a key argument\nusage: ai-fly key add <sk-aifly-key> --provider <id|alias>");
|
|
20
|
+
const provider = options.provider;
|
|
21
|
+
if (provider === void 0) throw new UsageError("error: --provider is required (endpointId, 8-char prefix, or alias)");
|
|
22
|
+
const out = ctxOut(ctx);
|
|
23
|
+
const root = consumersRoot(options.data, ctxHomedir(ctx));
|
|
24
|
+
const result = addKey(key, provider, root);
|
|
25
|
+
out(result.added ? "key added to keyring" : "key updated in keyring (existing entry)");
|
|
26
|
+
for (const line of formatKeyringSummary(result.ring)) out(line);
|
|
27
|
+
out("note: key id and group metadata are filled in on the next successful authorization");
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { run };
|