@iicp/client 0.7.85 → 0.7.87

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 CHANGED
@@ -39,7 +39,7 @@ What good looks like:
39
39
  ```bash
40
40
  iicp-node --help # shows query, serve, proxy, mcp-gateway, credits, ...
41
41
  which iicp-node # points to your Node/npm environment
42
- iicp-node --version # prints iicp-node 0.7.85 or newer
42
+ iicp-node --version # prints iicp-node 0.7.87 or newer
43
43
  ```
44
44
 
45
45
  The query command contacts the public directory, discovers a matching live node,
@@ -461,7 +461,10 @@ const node = new IicpNode({
461
461
  });
462
462
  ```
463
463
 
464
- > **Security note**: relay bind authentication is pending ([#510](https://github.com/RobLe3/iicp.network/issues/510)) — only run a relay accept port on networks you trust until the signed-bind mechanism ships.
464
+ Relay workers request short-lived directory-signed bind tickets when they have a saved node
465
+ token. Relay operators can enforce them with `IICP_RELAY_REQUIRE_BIND_TICKET=1` and the
466
+ directory's Ed25519 verification key in `IICP_RELAY_BIND_TICKET_PUBLIC_KEY`. Keep strict mode
467
+ enabled on public relays; unsigned compatibility mode is intended only for staged migration.
465
468
 
466
469
  ### Opt-out / override
467
470
 
@@ -477,6 +480,22 @@ IICP_AUTO_UPDATE_INTERVAL_S=3600 # update cadence in seconds; minimum 30
477
480
  IICP_RELAY_WORKER_ENDPOINT=host:9485 # specific relay instead of auto-elect
478
481
  ```
479
482
 
483
+ ### Publish a signed node policy
484
+
485
+ Operators can describe public handling rules in a local JSON file and have the client sign it
486
+ with their existing operator identity before registration:
487
+
488
+ ```bash
489
+ iicp-node serve --node my-node --policy-manifest ~/.iicp/node-policy.json
490
+ # or: IICP_POLICY_MANIFEST_FILE=~/.iicp/node-policy.json
491
+ ```
492
+
493
+ The source file stays local. The registration contains the public policy document, its public
494
+ operator key, timestamps, and detached Ed25519 signature—never the operator secret. The same
495
+ signed document is reused during recovery re-registration, so policy does not disappear when
496
+ a tunnel rotates. A signed declaration is tamper-evident operator evidence, not a legal or
497
+ privacy certification.
498
+
480
499
  ---
481
500
 
482
501
  ## Operator identity
@@ -501,6 +520,16 @@ and the identity, with its founder ordinal, is gone.
501
520
 
502
521
  Full guide: **[iicp.network/docs/operator-identity](https://iicp.network/docs/operator-identity)**
503
522
 
523
+ ### Operator data rights
524
+
525
+ You can request a portable, redacted record of the operator metadata held by a compatible directory without uploading your private identity key:
526
+
527
+ ```bash
528
+ iicp-node operator dsr export --output ~/iicp-operator-export.json
529
+ ```
530
+
531
+ The client obtains a short-lived challenge and signs it locally. The receipt excludes the private key, node tokens, prompt content, and contact details; it is saved owner-only on Unix. `restrict` and `anonymize` are explicit, confirmed requests and do not erase retention records that a directory must keep for security, fraud prevention, or legal obligations. See the [operator rights guide](https://iicp.network/operator/rights).
532
+
504
533
  ---
505
534
 
506
535
  ## SDK conformance
package/dist/cli.d.ts CHANGED
@@ -17,6 +17,7 @@ export interface ProviderAutoUpdateOptions {
17
17
  export declare function startProviderAutoUpdate(options?: ProviderAutoUpdateOptions): () => void;
18
18
  export interface ServeOpts {
19
19
  backendUrl: string;
20
+ policyManifest?: string;
20
21
  backendType: string;
21
22
  /** #5 — Bearer key for an auth-requiring OpenAI-compat backend (LM Studio, hosted). Empty = none. */
22
23
  backendApiKey: string;
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -164,6 +164,7 @@ function printHelp() {
164
164
  ` service Generate/install OS supervisor units for unattended node serving\n` +
165
165
  ` update Check whether a newer @iicp/client release is available\n` +
166
166
  ` operator rename <name> Change your public display_name (signed by your operator key)\n` +
167
+ ` operator dsr <action> Export, restrict, or anonymize your directory records\n` +
167
168
  ` operator encrypt Password-encrypt the operator secret at rest ($IICP_OPERATOR_PASSPHRASE)\n` +
168
169
  ` operator decrypt Remove at-rest encryption of the operator secret\n\n` +
169
170
  `Run an IICP provider node backed by an OpenAI-compatible server.\n\n` +
@@ -174,6 +175,7 @@ function printHelp() {
174
175
  ` --backend-url URL IICP_BACKEND_URL — Ollama / vLLM / LM Studio (default http://localhost:11434; anthropic → https://api.anthropic.com)\n` +
175
176
  ` --backend-type TYPE IICP_BACKEND_TYPE — openai_compat | vllm | llamacpp | anthropic (default openai_compat)\n` +
176
177
  ` --backend-api-key KEY IICP_BACKEND_API_KEY — Bearer key for an auth'd backend (LM Studio, hosted); anthropic uses it as x-api-key\n` +
178
+ ` --policy-manifest FILE IICP_POLICY_MANIFEST_FILE — sign and advertise a public node-policy JSON document\n` +
177
179
  ` --public-endpoint URL IICP_PUBLIC_ENDPOINT — externally reachable URL of this node\n` +
178
180
  ` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api)\n` +
179
181
  ` --region REGION IICP_REGION (e.g. us-east; unknown if unset)\n` +
@@ -196,8 +198,8 @@ function printHelp() {
196
198
  ` --relay-worker-endpoint H IICP_RELAY_WORKER_ENDPOINT — <host>:<port> of a relay node (R2 last-resort)\n` +
197
199
  ` --relay-capable IICP_RELAY_CAPABLE — advertise as relay server for CGNAT/tier-4 operators\n` +
198
200
  ` --relay-accept-port N IICP_RELAY_ACCEPT_PORT — TCP port for relay accept server (default 9485).\n` +
199
- ` Note: relay bind authentication is pending (#510) — only run a relay\n` +
200
- ` accept port on networks you trust until the signed-bind mechanism ships.\n` +
201
+ ` Public relays should set IICP_RELAY_REQUIRE_BIND_TICKET=1 and\n` +
202
+ ` IICP_RELAY_BIND_TICKET_PUBLIC_KEY for directory-signed one-use binds.\n` +
201
203
  ` --log-dir DIR IICP_LOG_DIR — directory for persistent log files (<node_id>.log + events.jsonl)\n` +
202
204
  ` --with-proxy IICP_WITH_PROXY — also run the loopback compat proxy (127.0.0.1:9483) in-process\n\n` +
203
205
  `query optional:\n` +
@@ -240,6 +242,7 @@ function printServeHelp() {
240
242
  ` --backend-url URL IICP_BACKEND_URL — Ollama / vLLM / LM Studio (default http://localhost:11434; anthropic → https://api.anthropic.com)\n` +
241
243
  ` --backend-type TYPE IICP_BACKEND_TYPE — openai_compat | vllm | llamacpp | anthropic\n` +
242
244
  ` --backend-api-key KEY IICP_BACKEND_API_KEY — Bearer key for auth'd backends\n` +
245
+ ` --policy-manifest FILE IICP_POLICY_MANIFEST_FILE — sign and advertise a public node-policy JSON document\n` +
243
246
  ` --public-endpoint URL IICP_PUBLIC_ENDPOINT — externally reachable URL of this node\n` +
244
247
  ` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api)\n` +
245
248
  ` --region REGION IICP_REGION (e.g. eu-central)\n` +
@@ -935,6 +938,7 @@ async function runServe(opts) {
935
938
  let _opDisplayName;
936
939
  let _opCreatedAt;
937
940
  let _opIntegrityHash;
941
+ let policyManifest;
938
942
  const _identityNotice = (0, identity_js_1.noIdentityNotice)(_op);
939
943
  if (_identityNotice !== null) {
940
944
  // #503 — anonymous registration accrues no founder/recognition standing;
@@ -946,7 +950,13 @@ async function runServe(opts) {
946
950
  _opDisplayName = _op.display_name || undefined;
947
951
  _opCreatedAt = _op.created_at;
948
952
  _opIntegrityHash = _op.operator_integrity_hash || undefined;
953
+ if (opts.policyManifest) {
954
+ const { loadAndSignPolicyManifest } = await import("./policy_manifest.js");
955
+ policyManifest = loadAndSignPolicyManifest(opts.policyManifest, _op);
956
+ }
949
957
  }
958
+ if (opts.policyManifest && !policyManifest)
959
+ throw new Error("--policy-manifest requires a key-backed operator identity");
950
960
  const node = new node_js_1.IicpNode({
951
961
  nodeId,
952
962
  endpoint: publicEndpoint,
@@ -964,6 +974,7 @@ async function runServe(opts) {
964
974
  operatorDisplayName: _opDisplayName,
965
975
  operatorCreatedAt: _opCreatedAt,
966
976
  operatorIntegrityHash: _opIntegrityHash,
977
+ policyManifest,
967
978
  // #494 — wire backend URL for live health_models probing in heartbeats.
968
979
  backendUrl: opts.backendUrl || undefined,
969
980
  backendApiKey: opts.backendApiKey || undefined,
@@ -1974,12 +1985,131 @@ function printOperatorHelp() {
1974
1985
  `Manage your operator identity.\n\n` +
1975
1986
  `subcommands:\n` +
1976
1987
  ` rename <name> Change your public display_name (signed by your operator key)\n` +
1988
+ ` dsr <action> Export, restrict, or anonymize your directory records\n` +
1977
1989
  ` encrypt Password-encrypt the operator secret at rest ($IICP_OPERATOR_PASSPHRASE)\n` +
1978
1990
  ` decrypt Remove at-rest encryption of the operator secret\n\n` +
1979
1991
  `operator rename options:\n` +
1980
1992
  ` --directory-url URL IICP directory base URL (defaults to env / iicp.network)\n` +
1981
1993
  ` -h, --help Show this help and exit\n`);
1982
1994
  }
1995
+ async function runOperatorDsr(argv) {
1996
+ const { values, positionals } = safeParseArgs({
1997
+ args: argv,
1998
+ options: {
1999
+ "directory-url": { type: "string" },
2000
+ "tracking-id": { type: "string" },
2001
+ output: { type: "string" },
2002
+ yes: { type: "boolean" },
2003
+ help: { type: "boolean", short: "h" },
2004
+ },
2005
+ allowPositionals: true,
2006
+ });
2007
+ if (values.help) {
2008
+ process.stdout.write("usage: iicp-node operator dsr <export|restrict|anonymize> [options]\n\n" +
2009
+ "The local operator key signs a one-use directory challenge. It never enters the browser or request body.\n\n" +
2010
+ " --output FILE Required for export; creates a new mode-0600 local file\n" +
2011
+ " --yes Confirm restrict or anonymize after the retained-record notice\n" +
2012
+ " --tracking-id ID Optional local reference\n" +
2013
+ " --directory-url URL IICP directory base URL\n");
2014
+ return 0;
2015
+ }
2016
+ const action = positionals[0];
2017
+ if (action !== "export" && action !== "restrict" && action !== "anonymize") {
2018
+ process.stderr.write("ERROR: usage: iicp-node operator dsr <export|restrict|anonymize> [options]\n");
2019
+ return 2;
2020
+ }
2021
+ const outputPath = values.output;
2022
+ if (action === "export" && !outputPath) {
2023
+ process.stderr.write("ERROR: export requires --output <new-file>; it is not printed to the terminal.\n");
2024
+ return 2;
2025
+ }
2026
+ if (outputPath && fs.existsSync(outputPath)) {
2027
+ process.stderr.write("ERROR: --output already exists; choose a new path to avoid replacing an export.\n");
2028
+ return 2;
2029
+ }
2030
+ if (action !== "export") {
2031
+ process.stderr.write("This action restricts or anonymizes directory records. Minimal signed ledger, security and accounting records may be retained and are explained in the receipt.\n");
2032
+ if (!values.yes) {
2033
+ process.stderr.write("Re-run with --yes to confirm.\n");
2034
+ return 2;
2035
+ }
2036
+ }
2037
+ const op = (0, identity_js_1.loadOperator)();
2038
+ if (!op) {
2039
+ process.stderr.write("ERROR: no operator identity — run `iicp-node init` first.\n");
2040
+ return 1;
2041
+ }
2042
+ if (!(0, identity_js_1.operatorIsKeyBacked)(op)) {
2043
+ process.stderr.write("ERROR: this legacy operator identity cannot sign a rights request; regenerate a key-backed identity.\n");
2044
+ return 1;
2045
+ }
2046
+ const directoryUrl = values["directory-url"] ?? process.env["IICP_DIRECTORY_URL"] ?? "https://iicp.network/api";
2047
+ const base = `${directoryUrl.replace(/\/+$/, "")}/v1/operator`;
2048
+ let response;
2049
+ try {
2050
+ const challenge = await fetch(`${base}/challenge`, {
2051
+ method: "POST",
2052
+ headers: { "Content-Type": "application/json" },
2053
+ body: JSON.stringify({ operator_pub: op.operator_id }),
2054
+ signal: AbortSignal.timeout(15000),
2055
+ });
2056
+ const challengeBody = (await challenge.json());
2057
+ if (!challenge.ok || typeof challengeBody.nonce !== "string") {
2058
+ throw new Error(challengeBody.error?.message ?? "directory challenge was incomplete");
2059
+ }
2060
+ const payload = {
2061
+ operator_pub: op.operator_id,
2062
+ nonce: challengeBody.nonce,
2063
+ ts: Math.floor(Date.now() / 1000),
2064
+ tracking_id: values["tracking-id"] ?? `dsr-${(0, node_crypto_1.randomUUID)()}`,
2065
+ };
2066
+ if (action !== "export")
2067
+ payload.confirm = true;
2068
+ payload.sig = (0, delegation_js_1.signOperatorSelfService)((0, identity_js_1.operatorSigningKey)(op), `dsr_${action}`, payload);
2069
+ response = await fetch(`${base}/dsr/${action}`, {
2070
+ method: "POST",
2071
+ headers: { "Content-Type": "application/json" },
2072
+ body: JSON.stringify(payload),
2073
+ signal: AbortSignal.timeout(15000),
2074
+ });
2075
+ }
2076
+ catch (err) {
2077
+ process.stderr.write(`ERROR: rights request failed: ${err instanceof Error ? err.message : String(err)}\n`);
2078
+ return 1;
2079
+ }
2080
+ let body;
2081
+ try {
2082
+ body = (await response.json());
2083
+ }
2084
+ catch {
2085
+ body = {};
2086
+ }
2087
+ if (!response.ok) {
2088
+ const err = body.error;
2089
+ process.stderr.write(`ERROR: directory rejected rights request: ${err?.message ?? response.status}\n`);
2090
+ return 1;
2091
+ }
2092
+ if (action === "export" && outputPath) {
2093
+ try {
2094
+ fs.writeFileSync(outputPath, `${JSON.stringify(body, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
2095
+ fs.chmodSync(outputPath, 0o600);
2096
+ }
2097
+ catch (err) {
2098
+ process.stderr.write(`ERROR: unable to save export safely: ${err instanceof Error ? err.message : String(err)}\n`);
2099
+ return 1;
2100
+ }
2101
+ process.stdout.write(`Saved redacted directory export to ${outputPath} (mode 0600).\n`);
2102
+ }
2103
+ else {
2104
+ process.stdout.write(`Directory ${action} request completed.\n`);
2105
+ }
2106
+ if (typeof body.tracking_id === "string")
2107
+ process.stdout.write(`Receipt reference: ${body.tracking_id}\n`);
2108
+ const retention = body.retention_notice ?? body.retention_reason;
2109
+ if (typeof retention === "string")
2110
+ process.stdout.write(`Retained records: ${retention}\n`);
2111
+ return 0;
2112
+ }
1983
2113
  function printOperatorEncryptHelp() {
1984
2114
  process.stdout.write(`usage: iicp-node operator encrypt\n\n` +
1985
2115
  `Password-encrypt the operator secret at rest. The command prompts for a passphrase\n` +
@@ -2020,6 +2150,8 @@ async function runOperator(argv) {
2020
2150
  throw new CliError(`unknown operator decrypt option '${rest[0]}'`);
2021
2151
  return runOperatorDecrypt();
2022
2152
  }
2153
+ if (sub === "dsr")
2154
+ return runOperatorDsr(argv.slice(1));
2023
2155
  if (sub !== "rename") {
2024
2156
  process.stderr.write(`unknown operator subcommand: ${sub}\n`);
2025
2157
  printOperatorHelp();
@@ -2500,6 +2632,7 @@ async function dispatch(argv) {
2500
2632
  options: {
2501
2633
  node: { type: "string" },
2502
2634
  "backend-url": { type: "string" },
2635
+ "policy-manifest": { type: "string" },
2503
2636
  "backend-type": { type: "string" },
2504
2637
  "backend-api-key": { type: "string" },
2505
2638
  model: { type: "string" },
@@ -2536,6 +2669,7 @@ async function dispatch(argv) {
2536
2669
  const opts = {
2537
2670
  node: values.node ?? envOr("IICP_NODE_NAME") ?? "",
2538
2671
  backendUrl: values["backend-url"] ?? envOr("IICP_BACKEND_URL") ?? "",
2672
+ policyManifest: values["policy-manifest"] ?? envOr("IICP_POLICY_MANIFEST_FILE") ?? "",
2539
2673
  backendType: values["backend-type"] ??
2540
2674
  envOr("IICP_BACKEND_TYPE", "openai_compat"),
2541
2675
  backendApiKey: values["backend-api-key"] ?? envOr("IICP_BACKEND_API_KEY") ?? "",