@pidgeon-ai/cli 1.36.1 → 1.38.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.
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  COMMANDS,
4
4
  __export
5
- } from "./chunk-W5VS5RM7.js";
5
+ } from "./chunk-UBA2RP6N.js";
6
6
 
7
7
  // ../sdk/dist/index.js
8
8
  var dist_exports = {};
@@ -67,6 +67,14 @@ __export(dist_exports, {
67
67
  eventsSocket: () => eventsSocket,
68
68
  eventsStream: () => eventsStream,
69
69
  exportMail: () => exportMail,
70
+ grantsCreate: () => grantsCreate,
71
+ grantsGet: () => grantsGet,
72
+ grantsList: () => grantsList,
73
+ grantsRevoke: () => grantsRevoke,
74
+ grantsStatus: () => grantsStatus,
75
+ grantsTemplatesCreate: () => grantsTemplatesCreate,
76
+ grantsTemplatesDelete: () => grantsTemplatesDelete,
77
+ grantsTemplatesList: () => grantsTemplatesList,
70
78
  identitiesAccessCreate: () => identitiesAccessCreate,
71
79
  identitiesAccessDelete: () => identitiesAccessDelete,
72
80
  identitiesAccessGet: () => identitiesAccessGet,
@@ -1025,6 +1033,10 @@ var logMonitorsDelete = (options) => (options.client ?? client).delete({
1025
1033
  url: "/log/monitors/{id}",
1026
1034
  ...options
1027
1035
  });
1036
+ var grantsStatus = (options) => (options.client ?? client).get({
1037
+ url: "/grants/{id}/status",
1038
+ ...options
1039
+ });
1028
1040
  var agentConnect = (options) => (options.client ?? client).post({
1029
1041
  url: "/agent/connect",
1030
1042
  ...options,
@@ -1201,6 +1213,49 @@ var labelsUpdate = (options) => (options.client ?? client).patch({
1201
1213
  ...options.headers
1202
1214
  }
1203
1215
  });
1216
+ var grantsTemplatesList = (options) => (options?.client ?? client).get({
1217
+ security: [{ scheme: "bearer", type: "http" }],
1218
+ url: "/grant-templates",
1219
+ ...options
1220
+ });
1221
+ var grantsTemplatesCreate = (options) => (options.client ?? client).post({
1222
+ security: [{ scheme: "bearer", type: "http" }],
1223
+ url: "/grant-templates",
1224
+ ...options,
1225
+ headers: {
1226
+ "Content-Type": "application/json",
1227
+ ...options.headers
1228
+ }
1229
+ });
1230
+ var grantsTemplatesDelete = (options) => (options.client ?? client).delete({
1231
+ security: [{ scheme: "bearer", type: "http" }],
1232
+ url: "/grant-templates/{id}",
1233
+ ...options
1234
+ });
1235
+ var grantsList = (options) => (options.client ?? client).get({
1236
+ security: [{ scheme: "bearer", type: "http" }],
1237
+ url: "/grants",
1238
+ ...options
1239
+ });
1240
+ var grantsCreate = (options) => (options.client ?? client).post({
1241
+ security: [{ scheme: "bearer", type: "http" }],
1242
+ url: "/grants",
1243
+ ...options,
1244
+ headers: {
1245
+ "Content-Type": "application/json",
1246
+ ...options.headers
1247
+ }
1248
+ });
1249
+ var grantsRevoke = (options) => (options.client ?? client).delete({
1250
+ security: [{ scheme: "bearer", type: "http" }],
1251
+ url: "/grants/{id}",
1252
+ ...options
1253
+ });
1254
+ var grantsGet = (options) => (options.client ?? client).get({
1255
+ security: [{ scheme: "bearer", type: "http" }],
1256
+ url: "/grants/{id}",
1257
+ ...options
1258
+ });
1204
1259
  var contactsList = (options) => (options?.client ?? client).get({
1205
1260
  security: [{ scheme: "bearer", type: "http" }],
1206
1261
  url: "/contacts",
@@ -1972,7 +2027,7 @@ function sleep(ms, signal) {
1972
2027
  });
1973
2028
  }
1974
2029
 
1975
- // ../id/dist/chunk-PM3YNIMV.js
2030
+ // ../id/dist/chunk-C5J6V6N2.js
1976
2031
  import { createHash } from "crypto";
1977
2032
  import { chmod, mkdir, readFile, readdir, writeFile } from "fs/promises";
1978
2033
  import { homedir } from "os";
@@ -2414,7 +2469,8 @@ async function signRequest(identity, input) {
2414
2469
  }
2415
2470
  const added = {
2416
2471
  "signature-agent": quote(`https://post.${identity.domain}`),
2417
- ...agent ? { from: agent } : {}
2472
+ ...agent ? { from: agent } : {},
2473
+ ...input.grants?.length ? { "post-grant": input.grants.map((jws) => jws.replace(/\s+/g, "")).join(", ") } : {}
2418
2474
  };
2419
2475
  const body = input.body === void 0 || input.body === null ? null : bytesOf(input.body);
2420
2476
  if (body && body.length > 0) added["content-digest"] = contentDigest(body);
@@ -2425,6 +2481,7 @@ async function signRequest(identity, input) {
2425
2481
  ...url.search ? [["@query", url.search]] : [],
2426
2482
  ["signature-agent", added["signature-agent"]],
2427
2483
  ...added.from ? [["from", added.from]] : [],
2484
+ ...added["post-grant"] ? [["post-grant", added["post-grant"]]] : [],
2428
2485
  ...added["content-digest"] ? [["content-digest", added["content-digest"]]] : []
2429
2486
  ];
2430
2487
  const params = `(${values.map(([name]) => quote(name)).join(" ")});created=${created};expires=${expires};keyid=${quote(identity.kid)};alg="ed25519";nonce=${quote(randomBytes(32).toString("base64"))};tag=${quote(input.tag ?? TAG)}`;
@@ -2468,15 +2525,15 @@ var IdClient = class {
2468
2525
  await signRequest(this.identity, { method, url, headers, body: text, tag: ISSUANCE_TAG })
2469
2526
  );
2470
2527
  const response = await this.fetcher(url, { method, headers, body: text ?? void 0 });
2471
- const json = await response.json().catch(() => null);
2472
- if (!response.ok || !json || json.data === void 0) {
2528
+ const json2 = await response.json().catch(() => null);
2529
+ if (!response.ok || !json2 || json2.data === void 0) {
2473
2530
  throw new IdApiError(
2474
2531
  response.status,
2475
- json?.error?.code,
2476
- json?.error?.message ?? `Pidgeon answered ${response.status}.`
2532
+ json2?.error?.code,
2533
+ json2?.error?.message ?? `Pidgeon answered ${response.status}.`
2477
2534
  );
2478
2535
  }
2479
- return json.data;
2536
+ return json2.data;
2480
2537
  }
2481
2538
  /** Claim the domain with this key. Idempotent: a second call returns the same records. */
2482
2539
  claim() {
@@ -2498,6 +2555,20 @@ var IdClient = class {
2498
2555
  revoke(name) {
2499
2556
  return this.call("DELETE", `/agents/${encodeURIComponent(name)}`);
2500
2557
  }
2558
+ /**
2559
+ * The domain grants one of its own agents, at once: what it may do, for
2560
+ * which customer, account or order, where, until when (at most 90 days).
2561
+ */
2562
+ grant(input) {
2563
+ return this.call("POST", "/grants", input);
2564
+ }
2565
+ /** Every token Sign in with Pidgeon issued `name`, taken back; its card and grants stand. */
2566
+ revokeSignIns(name) {
2567
+ return this.call("DELETE", `/agents/${encodeURIComponent(name)}/sign-ins`);
2568
+ }
2569
+ revokeGrant(id) {
2570
+ return this.call("DELETE", `/grants/${encodeURIComponent(id)}`);
2571
+ }
2501
2572
  /**
2502
2573
  * Publish `next` as the domain's key, signed by the current one. `proof`
2503
2574
  * is `next`'s own signature over the rotation, so nobody can rotate a
@@ -2517,6 +2588,136 @@ ${domain}
2517
2588
  ${from}
2518
2589
  ${to}`;
2519
2590
  }
2591
+ var TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange";
2592
+ var JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";
2593
+ var TOKEN_TYPE_JWT = "urn:ietf:params:oauth:token-type:jwt";
2594
+ var SignInError = class extends Error {
2595
+ constructor(step, status, code, message) {
2596
+ super(message);
2597
+ this.step = step;
2598
+ this.status = status;
2599
+ this.code = code;
2600
+ }
2601
+ step;
2602
+ status;
2603
+ code;
2604
+ };
2605
+ async function json(response) {
2606
+ return await response.json().catch(() => null) ?? {};
2607
+ }
2608
+ function form(values) {
2609
+ return new URLSearchParams(values).toString();
2610
+ }
2611
+ async function siteMetadata(site, fetcher = fetch) {
2612
+ const response = await fetcher(site, { headers: { accept: "application/json" } });
2613
+ const body = await json(response);
2614
+ if (!response.ok || typeof body.issuer !== "string" || typeof body.client_id !== "string") {
2615
+ throw new SignInError(
2616
+ "site",
2617
+ response.status,
2618
+ void 0,
2619
+ `${site} does not offer Sign in with Pidgeon.`
2620
+ );
2621
+ }
2622
+ return { issuer: body.issuer, client_id: body.client_id, token_endpoint: site };
2623
+ }
2624
+ async function fetchCard(agent, fetcher = fetch, postHost = (domain) => `https://post.${domain}`) {
2625
+ const domain = agent.split("@")[1] ?? "";
2626
+ const response = await fetcher(
2627
+ `${postHost(domain)}/.well-known/post/card?address=${encodeURIComponent(agent)}`
2628
+ );
2629
+ const body = await json(response);
2630
+ if (!response.ok || typeof body.jws !== "string") {
2631
+ throw new SignInError(
2632
+ "card",
2633
+ response.status,
2634
+ void 0,
2635
+ `${agent} has no card to sign in with.`
2636
+ );
2637
+ }
2638
+ return body.jws;
2639
+ }
2640
+ function signInWithPidgeon(identity, input) {
2641
+ return signIn(
2642
+ identity,
2643
+ input,
2644
+ async ({ tokenEndpoint, exchange, headers, agent }) => signRequest(identity, {
2645
+ method: "POST",
2646
+ url: tokenEndpoint,
2647
+ headers,
2648
+ body: exchange,
2649
+ agent,
2650
+ grants: input.grant ? [input.grant] : void 0
2651
+ })
2652
+ );
2653
+ }
2654
+ async function signIn(identity, input, authenticate) {
2655
+ const fetcher = input.fetch ?? fetch;
2656
+ const agent = input.agent.trim().toLowerCase();
2657
+ const site = await siteMetadata(input.site, fetcher);
2658
+ const card = input.card ?? await fetchCard(agent, fetcher, input.postHost);
2659
+ const discovery = await json(
2660
+ await fetcher(`${site.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`)
2661
+ );
2662
+ const tokenEndpoint = discovery.token_endpoint;
2663
+ if (typeof tokenEndpoint !== "string") {
2664
+ throw new SignInError("pidgeon", 0, void 0, `${site.issuer} has no token endpoint.`);
2665
+ }
2666
+ const exchange = form({
2667
+ grant_type: TOKEN_EXCHANGE,
2668
+ subject_token: card,
2669
+ subject_token_type: TOKEN_TYPE_JWT,
2670
+ audience: site.client_id,
2671
+ ...input.delegation ? { requested_token_type: TOKEN_TYPE_JWT } : {}
2672
+ });
2673
+ const headers = { "content-type": "application/x-www-form-urlencoded" };
2674
+ const issued = await fetcher(tokenEndpoint, {
2675
+ method: "POST",
2676
+ headers: {
2677
+ ...headers,
2678
+ ...await authenticate({ tokenEndpoint, exchange, headers, agent, site })
2679
+ },
2680
+ body: exchange
2681
+ });
2682
+ const tokens = await json(issued);
2683
+ if (!issued.ok || typeof tokens.id_token !== "string") {
2684
+ throw new SignInError(
2685
+ "pidgeon",
2686
+ issued.status,
2687
+ tokens.error,
2688
+ tokens.error_description ?? `Pidgeon answered ${issued.status}.`
2689
+ );
2690
+ }
2691
+ const bearer = form({ grant_type: JWT_BEARER, assertion: tokens.id_token });
2692
+ const answered = await fetcher(site.token_endpoint, {
2693
+ method: "POST",
2694
+ headers: {
2695
+ ...headers,
2696
+ ...await signRequest(identity, {
2697
+ method: "POST",
2698
+ url: site.token_endpoint,
2699
+ headers,
2700
+ body: bearer,
2701
+ agent
2702
+ })
2703
+ },
2704
+ body: bearer
2705
+ });
2706
+ const session = await json(answered);
2707
+ if (!answered.ok) {
2708
+ throw new SignInError(
2709
+ "site",
2710
+ answered.status,
2711
+ session.error,
2712
+ session.error_description ?? `The site answered ${answered.status}.`
2713
+ );
2714
+ }
2715
+ return {
2716
+ site: session,
2717
+ idToken: tokens.id_token,
2718
+ accessToken: tokens.access_token
2719
+ };
2720
+ }
2520
2721
  var UsageError = class extends Error {
2521
2722
  };
2522
2723
  var HELP = `pidgeon id \u2014 a signed identity for your agents, on your own domain. No account.
@@ -2527,6 +2728,13 @@ var HELP = `pidgeon id \u2014 a signed identity for your agents, on your own dom
2527
2728
  list Agents on the domain
2528
2729
  revoke <name> Withdraw an agent's card now
2529
2730
  rotate Replace the domain's key; the old one verifies for another day
2731
+ grant <name> Grant <name> what it may do, as the domain
2732
+ --scope bookings.change --label "manage bookings"
2733
+ --subject customer:42 --audience delta.com --days 30
2734
+ --grantor maria@example.com ask a person instead
2735
+ revoke-grant <id> Take a grant back now
2736
+ revoke-sign-ins <name> End every token <name> signed in with; its card and grants stand
2737
+ sign-in <site> Sign in to a site as an agent --as <name> --grant <jws>
2530
2738
 
2531
2739
  --domain <domain> Which domain, when this machine holds more than one
2532
2740
  --json Print the API's answer as JSON
@@ -2569,7 +2777,7 @@ async function runId(argv, env = process.env, options = {}) {
2569
2777
  const [command, ...rest] = positional;
2570
2778
  if (!command || flags.has("help") || command === "help") return HELP;
2571
2779
  const dir = identityDir(env);
2572
- const json = flags.has("json");
2780
+ const json2 = flags.has("json");
2573
2781
  const say = options.stderr ?? (() => {
2574
2782
  });
2575
2783
  const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -2600,7 +2808,7 @@ async function runId(argv, env = process.env, options = {}) {
2600
2808
  made = true;
2601
2809
  }
2602
2810
  let state = await client2(identity).claim();
2603
- if (json && (state.status === "verified" || flags.has("no-wait")))
2811
+ if (json2 && (state.status === "verified" || flags.has("no-wait")))
2604
2812
  return JSON.stringify(state, null, 2);
2605
2813
  const lines = [
2606
2814
  made ? `Made a signing key for ${domain} on this machine (${identity.kid}).` : `Using the key for ${domain} on this machine.`
@@ -2629,13 +2837,13 @@ Or add them by hand:` : "Add these records at your DNS provider:",
2629
2837
  }
2630
2838
  lines.length = 0;
2631
2839
  }
2632
- if (json) return JSON.stringify(state, null, 2);
2840
+ if (json2) return JSON.stringify(state, null, 2);
2633
2841
  return [...lines, `${domain} is proved. Next: pidgeon id issue support-agent`].join("\n").trim();
2634
2842
  }
2635
2843
  case "check": {
2636
2844
  const identity = rest[0] ? await loadIdentity(rest[0], { dir, env }) : await which();
2637
2845
  const state = await client2(identity).check();
2638
- if (json) return JSON.stringify(state, null, 2);
2846
+ if (json2) return JSON.stringify(state, null, 2);
2639
2847
  return [
2640
2848
  `${state.domain}: ${state.status === "verified" ? "proved" : state.status}`,
2641
2849
  ...describeRecords(state)
@@ -2655,7 +2863,7 @@ Or add them by hand:` : "Add these records at your DNS provider:",
2655
2863
  ...kind ? { kind } : {},
2656
2864
  ...typeof onBehalfOf === "string" ? { on_behalf_of: onBehalfOf } : {}
2657
2865
  });
2658
- if (json) return JSON.stringify(card, null, 2);
2866
+ if (json2) return JSON.stringify(card, null, 2);
2659
2867
  return [
2660
2868
  `${card.card.address} has a card: ${card.url}`,
2661
2869
  `Found from the address at https://post.${identity.domain}/.well-known/post/card?address=${card.card.address}`,
@@ -2664,7 +2872,7 @@ Or add them by hand:` : "Add these records at your DNS provider:",
2664
2872
  }
2665
2873
  case "list": {
2666
2874
  const agents = await client2(await which()).list();
2667
- if (json) return JSON.stringify(agents, null, 2);
2875
+ if (json2) return JSON.stringify(agents, null, 2);
2668
2876
  if (agents.length === 0)
2669
2877
  return "No agents yet. `pidgeon id issue <name>` gives one a card.";
2670
2878
  return agents.map((agent) => `${agent.address} ${agent.kind} ${agent.card_url}`).join("\n");
@@ -2673,7 +2881,7 @@ Or add them by hand:` : "Add these records at your DNS provider:",
2673
2881
  const name = rest[0]?.trim().toLowerCase();
2674
2882
  if (!name) throw new UsageError("Name the agent: `pidgeon id revoke support-agent`.");
2675
2883
  const revoked = await client2(await which()).revoke(name);
2676
- if (json) return JSON.stringify(revoked, null, 2);
2884
+ if (json2) return JSON.stringify(revoked, null, 2);
2677
2885
  return `${revoked.address} has no card from now on. Copies already fetched lapse within a day.`;
2678
2886
  }
2679
2887
  case "rotate": {
@@ -2682,9 +2890,89 @@ Or add them by hand:` : "Add these records at your DNS provider:",
2682
2890
  const state = await client2(current).rotate(next);
2683
2891
  next.previous = { kid: current.kid, publicKey: current.publicKey };
2684
2892
  await saveIdentity(next, dir);
2685
- if (json) return JSON.stringify(state, null, 2);
2893
+ if (json2) return JSON.stringify(state, null, 2);
2686
2894
  return `${current.domain} signs with ${next.kid} now. ${current.kid} still verifies for a day.`;
2687
2895
  }
2896
+ case "grant": {
2897
+ const name = rest[0]?.trim().toLowerCase();
2898
+ const scope = flags.get("scope");
2899
+ const label = flags.get("label");
2900
+ const subject = flags.get("subject");
2901
+ if (!name || typeof scope !== "string" || typeof label !== "string") {
2902
+ throw new UsageError(
2903
+ 'Name the agent and the scope: `pidgeon id grant support-agent --scope bookings.change --label "manage bookings" --subject customer:42`.'
2904
+ );
2905
+ }
2906
+ const [type, ...idParts] = typeof subject === "string" ? subject.split(":") : [];
2907
+ if (!type || idParts.length === 0 || !["customer", "account", "order"].includes(type)) {
2908
+ throw new UsageError("--subject is customer:<id>, account:<id> or order:<id>.");
2909
+ }
2910
+ const days = flags.get("days");
2911
+ const audience = flags.get("audience");
2912
+ const grantor = flags.get("grantor");
2913
+ const grant = await client2(await which()).grant({
2914
+ agent: name,
2915
+ scope: { name: scope, label },
2916
+ subject: { type, id: idParts.join(":") },
2917
+ ...typeof audience === "string" ? { audience } : {},
2918
+ ...typeof grantor === "string" ? { grantor: { email: grantor } } : {},
2919
+ ...typeof days === "string" ? { expires_at: new Date(Date.now() + Number(days) * 864e5).toISOString() } : {}
2920
+ });
2921
+ if (json2) return JSON.stringify(grant, null, 2);
2922
+ if (grant.consent_url) {
2923
+ return [
2924
+ `Asked ${typeof grantor === "string" ? grantor : "them"} to let ${grant.agent} ${grant.scope.label} for ${grant.subject.type} ${grant.subject.id}.`,
2925
+ `Send them this link; nothing is allowed until they answer it: ${grant.consent_url}`,
2926
+ `Grant ${grant.id}. \`pidgeon id revoke-grant ${grant.id}\` withdraws the request.`
2927
+ ].join("\n");
2928
+ }
2929
+ return [
2930
+ `${grant.agent} may ${grant.scope.label} for ${grant.subject.type} ${grant.subject.id}${grant.audience ? ` at ${grant.audience}` : ""}, until ${grant.expires_at}.`,
2931
+ `Grant ${grant.id}. The agent carries it as Post-Grant; --json prints the JWS.`
2932
+ ].join("\n");
2933
+ }
2934
+ case "revoke-grant": {
2935
+ const grantId = rest[0]?.trim();
2936
+ if (!grantId) throw new UsageError("Name the grant: `pidgeon id revoke-grant <id>`.");
2937
+ const revoked = await client2(await which()).revokeGrant(grantId);
2938
+ if (json2) return JSON.stringify(revoked, null, 2);
2939
+ return `Grant ${revoked.id} is revoked. The next check of it fails.`;
2940
+ }
2941
+ case "revoke-sign-ins": {
2942
+ const name = rest[0]?.trim().toLowerCase();
2943
+ if (!name)
2944
+ throw new UsageError("Name the agent: `pidgeon id revoke-sign-ins support-agent`.");
2945
+ const done = await client2(await which()).revokeSignIns(name);
2946
+ if (json2) return JSON.stringify(done, null, 2);
2947
+ return `${done.address}: ${done.revoked} token${done.revoked === 1 ? "" : "s"} taken back. Its card and grants stand.`;
2948
+ }
2949
+ case "sign-in": {
2950
+ const site = rest[0];
2951
+ const name = flags.get("as");
2952
+ if (!site || typeof name !== "string") {
2953
+ throw new UsageError(
2954
+ "Name the site and the agent: `pidgeon id sign-in https://site.example/api/auth/pidgeon/token --as support-agent`."
2955
+ );
2956
+ }
2957
+ const identity = await which();
2958
+ const card = await client2(identity).issue({ name: name.trim().toLowerCase() });
2959
+ const grant = flags.get("grant");
2960
+ try {
2961
+ const signedIn = await signInWithPidgeon(identity, {
2962
+ agent: card.card.address,
2963
+ site,
2964
+ card: card.jws,
2965
+ ...typeof grant === "string" ? { grant } : {},
2966
+ ...options.fetch ? { fetch: options.fetch } : {}
2967
+ });
2968
+ if (json2) return JSON.stringify(signedIn.site, null, 2);
2969
+ return `Signed in to ${new URL(site).host} as ${card.card.address}.
2970
+ ${JSON.stringify(signedIn.site, null, 2)}`;
2971
+ } catch (error) {
2972
+ if (error instanceof SignInError) throw new Error(`${error.step}: ${error.message}`);
2973
+ throw error;
2974
+ }
2975
+ }
2688
2976
  default:
2689
2977
  throw new UsageError(`No such command "${command}".
2690
2978
 
@@ -2890,4 +3178,4 @@ export {
2890
3178
  find,
2891
3179
  run
2892
3180
  };
2893
- //# sourceMappingURL=chunk-FA77SSPH.js.map
3181
+ //# sourceMappingURL=chunk-ZDJG3Z7B.js.map