@pidgeon-ai/cli 1.38.0 → 1.40.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 CHANGED
@@ -40,6 +40,21 @@ one request open until mail arrives, and prints it with `extracted.codes` and
40
40
  describes one command, with its endpoint and its flags. Documentation:
41
41
  [pidgeon.ai/docs/cli](https://pidgeon.ai/docs/cli).
42
42
 
43
+ ## Check who sent something
44
+
45
+ `pidgeon id` needs no key. It checks a message, or signs a request as one of
46
+ your agents:
47
+
48
+ ```sh
49
+ pidgeon id verify message.eml
50
+ pidgeon id curl -I https://shop.example --as sunny
51
+ ```
52
+
53
+ `verify` asks the public verifier and says whether the signature holds, which
54
+ agent sent it, who it acts for and whether the body changed. `curl` signs the
55
+ request the way Web Bot Auth asks and prints what it sent. `pidgeon id --help`
56
+ lists the rest, including `init` for your own domain.
57
+
43
58
  ## Flags
44
59
 
45
60
  - Repeat a list flag: `--to one@example.com --to two@example.com`.
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  COMMANDS,
4
4
  __export
5
- } from "./chunk-UBA2RP6N.js";
5
+ } from "./chunk-OETRER3H.js";
6
6
 
7
7
  // ../sdk/dist/index.js
8
8
  var dist_exports = {};
@@ -2027,7 +2027,7 @@ function sleep(ms, signal) {
2027
2027
  });
2028
2028
  }
2029
2029
 
2030
- // ../id/dist/chunk-C5J6V6N2.js
2030
+ // ../id/dist/chunk-2NWPDKWJ.js
2031
2031
  import { createHash } from "crypto";
2032
2032
  import { chmod, mkdir, readFile, readdir, writeFile } from "fs/promises";
2033
2033
  import { homedir } from "os";
@@ -2496,6 +2496,34 @@ async function signRequest(identity, input) {
2496
2496
  signature: `${label}=:${Buffer.from(signature).toString("base64")}:`
2497
2497
  };
2498
2498
  }
2499
+ function signedFetch(identity, options) {
2500
+ const base = options.fetch ?? globalThis.fetch;
2501
+ return async (resource, init) => {
2502
+ const request = new Request(resource, init);
2503
+ const body = request.body ? new Uint8Array(await request.arrayBuffer()) : null;
2504
+ const headers = {};
2505
+ request.headers.forEach((value, name) => {
2506
+ headers[name] = value;
2507
+ });
2508
+ const signature = await signRequest(identity, {
2509
+ method: request.method,
2510
+ url: request.url,
2511
+ headers,
2512
+ body,
2513
+ agent: options.agent,
2514
+ grants: options.grants
2515
+ });
2516
+ const signedHeaders = new Headers(request.headers);
2517
+ for (const [name, value] of Object.entries(signature)) signedHeaders.set(name, value);
2518
+ return base(request.url, {
2519
+ method: request.method,
2520
+ headers: signedHeaders,
2521
+ body: body && body.length > 0 ? body : void 0,
2522
+ redirect: request.redirect,
2523
+ signal: request.signal
2524
+ });
2525
+ };
2526
+ }
2499
2527
  var ISSUANCE_TAG = "pidgeon-id";
2500
2528
  var DEFAULT_ID_URL = "https://pidgeon.ai/api/id";
2501
2529
  var IdApiError = class extends Error {
@@ -2735,6 +2763,8 @@ var HELP = `pidgeon id \u2014 a signed identity for your agents, on your own dom
2735
2763
  revoke-grant <id> Take a grant back now
2736
2764
  revoke-sign-ins <name> End every token <name> signed in with; its card and grants stand
2737
2765
  sign-in <site> Sign in to a site as an agent --as <name> --grant <jws>
2766
+ verify [<file>] Check a message's signatures (a file, or standard input). No key
2767
+ curl [-I] <url> Send a request signed as an agent --as <name>
2738
2768
 
2739
2769
  --domain <domain> Which domain, when this machine holds more than one
2740
2770
  --json Print the API's answer as JSON
@@ -2870,6 +2900,69 @@ Or add them by hand:` : "Add these records at your DNS provider:",
2870
2900
  'It renews itself. Sign with it: import { loadIdentity, signEmail, signedFetch } from "@pidgeon-ai/id".'
2871
2901
  ].join("\n");
2872
2902
  }
2903
+ case "verify": {
2904
+ const file = rest[0];
2905
+ const message = file ? await (await import("fs/promises")).readFile(file, "utf8") : await (options.stdin ?? readStdin)();
2906
+ if (!message.trim()) {
2907
+ throw new UsageError("Give it a message: `pidgeon id verify < message.eml`.");
2908
+ }
2909
+ const response = await (options.fetch ?? fetch)(
2910
+ `${new URL(baseUrl(env)).origin}/api/v1/verify`,
2911
+ {
2912
+ method: "POST",
2913
+ headers: { "content-type": "application/json" },
2914
+ body: JSON.stringify({ message })
2915
+ }
2916
+ );
2917
+ const answer = await response.json();
2918
+ if (!response.ok || !answer.data) {
2919
+ throw new UsageError(
2920
+ answer.error?.message ?? `The verifier answered ${response.status}.`
2921
+ );
2922
+ }
2923
+ if (json2) return JSON.stringify(answer.data, null, 2);
2924
+ return describeVerification(answer.data);
2925
+ }
2926
+ case "curl": {
2927
+ const head = rest[0] === "-I";
2928
+ const target = (head ? rest[1] : rest[0])?.trim();
2929
+ if (!target) throw new UsageError("Name a URL: `pidgeon id curl -I shop.example`.");
2930
+ const url = /^https?:\/\//.test(target) ? target : `https://${target}`;
2931
+ const identity = await which();
2932
+ const named = flags.get("as");
2933
+ let agent;
2934
+ if (typeof named === "string") {
2935
+ agent = named.includes("@") ? named : `${named}@${identity.domain}`;
2936
+ } else {
2937
+ const agents = await client2(identity).list();
2938
+ if (agents.length !== 1) {
2939
+ throw new UsageError("Say which agent signs, with --as <name>.");
2940
+ }
2941
+ agent = agents[0].address;
2942
+ }
2943
+ const sent = {};
2944
+ const signed = signedFetch(identity, {
2945
+ agent,
2946
+ fetch: async (resource, init) => {
2947
+ new Headers(init?.headers).forEach((value, name) => {
2948
+ if (/^(signature|signature-input|signature-agent|from|content-digest)$/.test(name)) {
2949
+ sent[name] = value;
2950
+ }
2951
+ });
2952
+ return (options.fetch ?? fetch)(resource, init);
2953
+ }
2954
+ });
2955
+ const response = await signed(url, { method: head ? "HEAD" : "GET" });
2956
+ const lines = Object.entries(sent).map(
2957
+ ([name, value]) => `> ${capitalise(name)}: ${value}`
2958
+ );
2959
+ lines.push(`< ${response.status} ${response.statusText}`.trimEnd());
2960
+ if (head) {
2961
+ response.headers.forEach((value, name) => lines.push(`< ${capitalise(name)}: ${value}`));
2962
+ return lines.join("\n");
2963
+ }
2964
+ return [...lines, "", await response.text()].join("\n");
2965
+ }
2873
2966
  case "list": {
2874
2967
  const agents = await client2(await which()).list();
2875
2968
  if (json2) return JSON.stringify(agents, null, 2);
@@ -2985,6 +3078,39 @@ ${HELP}`);
2985
3078
  throw error;
2986
3079
  }
2987
3080
  }
3081
+ function describeVerification(v) {
3082
+ const signer = v.signatures.envelope.domain ?? v.signatures.declaration.domain;
3083
+ const signed = v.signatures.envelope.result === "pass" || v.signatures.declaration.result === "pass";
3084
+ const broken = v.signatures.envelope.result === "fail" || v.signatures.declaration.result === "fail";
3085
+ const row = (ok, label, value) => `${ok === null ? "\xB7" : ok ? "\u2713" : "\u2717"} ${label.padEnd(10)} ${value}`;
3086
+ return [
3087
+ row(
3088
+ signed && !broken,
3089
+ "signature",
3090
+ signed && !broken ? `valid, ${signer}` : broken ? `broken${signer ? `, ${signer}` : ""}` : "none"
3091
+ ),
3092
+ row(v.is_agent, "agent", v.sender?.address ?? "not declared"),
3093
+ row(v.sender?.on_behalf_of ? true : null, "acts for", v.sender?.on_behalf_of ?? "not said"),
3094
+ row(
3095
+ v.body === "not_checked" ? null : v.body === "intact",
3096
+ "body",
3097
+ v.body === "intact" ? "as it was sent" : v.body === "changed" ? "changed since" : "not checked"
3098
+ ),
3099
+ "",
3100
+ v.advice
3101
+ ].join("\n");
3102
+ }
3103
+ function capitalise(name) {
3104
+ return name.replace(
3105
+ /(^|-)([a-z])/g,
3106
+ (_, dash, letter) => dash + letter.toUpperCase()
3107
+ );
3108
+ }
3109
+ async function readStdin() {
3110
+ const chunks = [];
3111
+ for await (const chunk of process.stdin) chunks.push(chunk);
3112
+ return Buffer.concat(chunks).toString("utf8");
3113
+ }
2988
3114
 
2989
3115
  // src/index.ts
2990
3116
  var DEFAULT_BASE_URL = "https://pidgeon.ai/api/v1";
@@ -3178,4 +3304,4 @@ export {
3178
3304
  find,
3179
3305
  run
3180
3306
  };
3181
- //# sourceMappingURL=chunk-ZDJG3Z7B.js.map
3307
+ //# sourceMappingURL=chunk-D2BUTVIS.js.map