@pidgeon-ai/cli 1.37.0 → 1.39.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`.
|
|
@@ -2027,7 +2027,7 @@ function sleep(ms, signal) {
|
|
|
2027
2027
|
});
|
|
2028
2028
|
}
|
|
2029
2029
|
|
|
2030
|
-
// ../id/dist/chunk-
|
|
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 {
|
|
@@ -2562,6 +2590,10 @@ var IdClient = class {
|
|
|
2562
2590
|
grant(input) {
|
|
2563
2591
|
return this.call("POST", "/grants", input);
|
|
2564
2592
|
}
|
|
2593
|
+
/** Every token Sign in with Pidgeon issued `name`, taken back; its card and grants stand. */
|
|
2594
|
+
revokeSignIns(name) {
|
|
2595
|
+
return this.call("DELETE", `/agents/${encodeURIComponent(name)}/sign-ins`);
|
|
2596
|
+
}
|
|
2565
2597
|
revokeGrant(id) {
|
|
2566
2598
|
return this.call("DELETE", `/grants/${encodeURIComponent(id)}`);
|
|
2567
2599
|
}
|
|
@@ -2633,7 +2665,21 @@ async function fetchCard(agent, fetcher = fetch, postHost = (domain) => `https:/
|
|
|
2633
2665
|
}
|
|
2634
2666
|
return body.jws;
|
|
2635
2667
|
}
|
|
2636
|
-
|
|
2668
|
+
function signInWithPidgeon(identity, input) {
|
|
2669
|
+
return signIn(
|
|
2670
|
+
identity,
|
|
2671
|
+
input,
|
|
2672
|
+
async ({ tokenEndpoint, exchange, headers, agent }) => signRequest(identity, {
|
|
2673
|
+
method: "POST",
|
|
2674
|
+
url: tokenEndpoint,
|
|
2675
|
+
headers,
|
|
2676
|
+
body: exchange,
|
|
2677
|
+
agent,
|
|
2678
|
+
grants: input.grant ? [input.grant] : void 0
|
|
2679
|
+
})
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
async function signIn(identity, input, authenticate) {
|
|
2637
2683
|
const fetcher = input.fetch ?? fetch;
|
|
2638
2684
|
const agent = input.agent.trim().toLowerCase();
|
|
2639
2685
|
const site = await siteMetadata(input.site, fetcher);
|
|
@@ -2649,21 +2695,15 @@ async function signInWithPidgeon(identity, input) {
|
|
|
2649
2695
|
grant_type: TOKEN_EXCHANGE,
|
|
2650
2696
|
subject_token: card,
|
|
2651
2697
|
subject_token_type: TOKEN_TYPE_JWT,
|
|
2652
|
-
audience: site.client_id
|
|
2698
|
+
audience: site.client_id,
|
|
2699
|
+
...input.delegation ? { requested_token_type: TOKEN_TYPE_JWT } : {}
|
|
2653
2700
|
});
|
|
2654
2701
|
const headers = { "content-type": "application/x-www-form-urlencoded" };
|
|
2655
2702
|
const issued = await fetcher(tokenEndpoint, {
|
|
2656
2703
|
method: "POST",
|
|
2657
2704
|
headers: {
|
|
2658
2705
|
...headers,
|
|
2659
|
-
...await
|
|
2660
|
-
method: "POST",
|
|
2661
|
-
url: tokenEndpoint,
|
|
2662
|
-
headers,
|
|
2663
|
-
body: exchange,
|
|
2664
|
-
agent,
|
|
2665
|
-
grants: input.grant ? [input.grant] : void 0
|
|
2666
|
-
})
|
|
2706
|
+
...await authenticate({ tokenEndpoint, exchange, headers, agent, site })
|
|
2667
2707
|
},
|
|
2668
2708
|
body: exchange
|
|
2669
2709
|
});
|
|
@@ -2719,8 +2759,12 @@ var HELP = `pidgeon id \u2014 a signed identity for your agents, on your own dom
|
|
|
2719
2759
|
grant <name> Grant <name> what it may do, as the domain
|
|
2720
2760
|
--scope bookings.change --label "manage bookings"
|
|
2721
2761
|
--subject customer:42 --audience delta.com --days 30
|
|
2762
|
+
--grantor maria@example.com ask a person instead
|
|
2722
2763
|
revoke-grant <id> Take a grant back now
|
|
2764
|
+
revoke-sign-ins <name> End every token <name> signed in with; its card and grants stand
|
|
2723
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>
|
|
2724
2768
|
|
|
2725
2769
|
--domain <domain> Which domain, when this machine holds more than one
|
|
2726
2770
|
--json Print the API's answer as JSON
|
|
@@ -2856,6 +2900,69 @@ Or add them by hand:` : "Add these records at your DNS provider:",
|
|
|
2856
2900
|
'It renews itself. Sign with it: import { loadIdentity, signEmail, signedFetch } from "@pidgeon-ai/id".'
|
|
2857
2901
|
].join("\n");
|
|
2858
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
|
+
}
|
|
2859
2966
|
case "list": {
|
|
2860
2967
|
const agents = await client2(await which()).list();
|
|
2861
2968
|
if (json2) return JSON.stringify(agents, null, 2);
|
|
@@ -2895,14 +3002,23 @@ Or add them by hand:` : "Add these records at your DNS provider:",
|
|
|
2895
3002
|
}
|
|
2896
3003
|
const days = flags.get("days");
|
|
2897
3004
|
const audience = flags.get("audience");
|
|
3005
|
+
const grantor = flags.get("grantor");
|
|
2898
3006
|
const grant = await client2(await which()).grant({
|
|
2899
3007
|
agent: name,
|
|
2900
3008
|
scope: { name: scope, label },
|
|
2901
3009
|
subject: { type, id: idParts.join(":") },
|
|
2902
3010
|
...typeof audience === "string" ? { audience } : {},
|
|
3011
|
+
...typeof grantor === "string" ? { grantor: { email: grantor } } : {},
|
|
2903
3012
|
...typeof days === "string" ? { expires_at: new Date(Date.now() + Number(days) * 864e5).toISOString() } : {}
|
|
2904
3013
|
});
|
|
2905
3014
|
if (json2) return JSON.stringify(grant, null, 2);
|
|
3015
|
+
if (grant.consent_url) {
|
|
3016
|
+
return [
|
|
3017
|
+
`Asked ${typeof grantor === "string" ? grantor : "them"} to let ${grant.agent} ${grant.scope.label} for ${grant.subject.type} ${grant.subject.id}.`,
|
|
3018
|
+
`Send them this link; nothing is allowed until they answer it: ${grant.consent_url}`,
|
|
3019
|
+
`Grant ${grant.id}. \`pidgeon id revoke-grant ${grant.id}\` withdraws the request.`
|
|
3020
|
+
].join("\n");
|
|
3021
|
+
}
|
|
2906
3022
|
return [
|
|
2907
3023
|
`${grant.agent} may ${grant.scope.label} for ${grant.subject.type} ${grant.subject.id}${grant.audience ? ` at ${grant.audience}` : ""}, until ${grant.expires_at}.`,
|
|
2908
3024
|
`Grant ${grant.id}. The agent carries it as Post-Grant; --json prints the JWS.`
|
|
@@ -2915,6 +3031,14 @@ Or add them by hand:` : "Add these records at your DNS provider:",
|
|
|
2915
3031
|
if (json2) return JSON.stringify(revoked, null, 2);
|
|
2916
3032
|
return `Grant ${revoked.id} is revoked. The next check of it fails.`;
|
|
2917
3033
|
}
|
|
3034
|
+
case "revoke-sign-ins": {
|
|
3035
|
+
const name = rest[0]?.trim().toLowerCase();
|
|
3036
|
+
if (!name)
|
|
3037
|
+
throw new UsageError("Name the agent: `pidgeon id revoke-sign-ins support-agent`.");
|
|
3038
|
+
const done = await client2(await which()).revokeSignIns(name);
|
|
3039
|
+
if (json2) return JSON.stringify(done, null, 2);
|
|
3040
|
+
return `${done.address}: ${done.revoked} token${done.revoked === 1 ? "" : "s"} taken back. Its card and grants stand.`;
|
|
3041
|
+
}
|
|
2918
3042
|
case "sign-in": {
|
|
2919
3043
|
const site = rest[0];
|
|
2920
3044
|
const name = flags.get("as");
|
|
@@ -2954,6 +3078,39 @@ ${HELP}`);
|
|
|
2954
3078
|
throw error;
|
|
2955
3079
|
}
|
|
2956
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
|
+
}
|
|
2957
3114
|
|
|
2958
3115
|
// src/index.ts
|
|
2959
3116
|
var DEFAULT_BASE_URL = "https://pidgeon.ai/api/v1";
|
|
@@ -3147,4 +3304,4 @@ export {
|
|
|
3147
3304
|
find,
|
|
3148
3305
|
run
|
|
3149
3306
|
};
|
|
3150
|
-
//# sourceMappingURL=chunk-
|
|
3307
|
+
//# sourceMappingURL=chunk-F6XZFMGH.js.map
|