@pidgeon-ai/cli 1.36.0 → 1.37.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-EAXZTLYY.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,16 @@ 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
+ revokeGrant(id) {
2566
+ return this.call("DELETE", `/grants/${encodeURIComponent(id)}`);
2567
+ }
2501
2568
  /**
2502
2569
  * Publish `next` as the domain's key, signed by the current one. `proof`
2503
2570
  * is `next`'s own signature over the rotation, so nobody can rotate a
@@ -2517,6 +2584,128 @@ ${domain}
2517
2584
  ${from}
2518
2585
  ${to}`;
2519
2586
  }
2587
+ var TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange";
2588
+ var JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";
2589
+ var TOKEN_TYPE_JWT = "urn:ietf:params:oauth:token-type:jwt";
2590
+ var SignInError = class extends Error {
2591
+ constructor(step, status, code, message) {
2592
+ super(message);
2593
+ this.step = step;
2594
+ this.status = status;
2595
+ this.code = code;
2596
+ }
2597
+ step;
2598
+ status;
2599
+ code;
2600
+ };
2601
+ async function json(response) {
2602
+ return await response.json().catch(() => null) ?? {};
2603
+ }
2604
+ function form(values) {
2605
+ return new URLSearchParams(values).toString();
2606
+ }
2607
+ async function siteMetadata(site, fetcher = fetch) {
2608
+ const response = await fetcher(site, { headers: { accept: "application/json" } });
2609
+ const body = await json(response);
2610
+ if (!response.ok || typeof body.issuer !== "string" || typeof body.client_id !== "string") {
2611
+ throw new SignInError(
2612
+ "site",
2613
+ response.status,
2614
+ void 0,
2615
+ `${site} does not offer Sign in with Pidgeon.`
2616
+ );
2617
+ }
2618
+ return { issuer: body.issuer, client_id: body.client_id, token_endpoint: site };
2619
+ }
2620
+ async function fetchCard(agent, fetcher = fetch, postHost = (domain) => `https://post.${domain}`) {
2621
+ const domain = agent.split("@")[1] ?? "";
2622
+ const response = await fetcher(
2623
+ `${postHost(domain)}/.well-known/post/card?address=${encodeURIComponent(agent)}`
2624
+ );
2625
+ const body = await json(response);
2626
+ if (!response.ok || typeof body.jws !== "string") {
2627
+ throw new SignInError(
2628
+ "card",
2629
+ response.status,
2630
+ void 0,
2631
+ `${agent} has no card to sign in with.`
2632
+ );
2633
+ }
2634
+ return body.jws;
2635
+ }
2636
+ async function signInWithPidgeon(identity, input) {
2637
+ const fetcher = input.fetch ?? fetch;
2638
+ const agent = input.agent.trim().toLowerCase();
2639
+ const site = await siteMetadata(input.site, fetcher);
2640
+ const card = input.card ?? await fetchCard(agent, fetcher, input.postHost);
2641
+ const discovery = await json(
2642
+ await fetcher(`${site.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`)
2643
+ );
2644
+ const tokenEndpoint = discovery.token_endpoint;
2645
+ if (typeof tokenEndpoint !== "string") {
2646
+ throw new SignInError("pidgeon", 0, void 0, `${site.issuer} has no token endpoint.`);
2647
+ }
2648
+ const exchange = form({
2649
+ grant_type: TOKEN_EXCHANGE,
2650
+ subject_token: card,
2651
+ subject_token_type: TOKEN_TYPE_JWT,
2652
+ audience: site.client_id
2653
+ });
2654
+ const headers = { "content-type": "application/x-www-form-urlencoded" };
2655
+ const issued = await fetcher(tokenEndpoint, {
2656
+ method: "POST",
2657
+ headers: {
2658
+ ...headers,
2659
+ ...await signRequest(identity, {
2660
+ method: "POST",
2661
+ url: tokenEndpoint,
2662
+ headers,
2663
+ body: exchange,
2664
+ agent,
2665
+ grants: input.grant ? [input.grant] : void 0
2666
+ })
2667
+ },
2668
+ body: exchange
2669
+ });
2670
+ const tokens = await json(issued);
2671
+ if (!issued.ok || typeof tokens.id_token !== "string") {
2672
+ throw new SignInError(
2673
+ "pidgeon",
2674
+ issued.status,
2675
+ tokens.error,
2676
+ tokens.error_description ?? `Pidgeon answered ${issued.status}.`
2677
+ );
2678
+ }
2679
+ const bearer = form({ grant_type: JWT_BEARER, assertion: tokens.id_token });
2680
+ const answered = await fetcher(site.token_endpoint, {
2681
+ method: "POST",
2682
+ headers: {
2683
+ ...headers,
2684
+ ...await signRequest(identity, {
2685
+ method: "POST",
2686
+ url: site.token_endpoint,
2687
+ headers,
2688
+ body: bearer,
2689
+ agent
2690
+ })
2691
+ },
2692
+ body: bearer
2693
+ });
2694
+ const session = await json(answered);
2695
+ if (!answered.ok) {
2696
+ throw new SignInError(
2697
+ "site",
2698
+ answered.status,
2699
+ session.error,
2700
+ session.error_description ?? `The site answered ${answered.status}.`
2701
+ );
2702
+ }
2703
+ return {
2704
+ site: session,
2705
+ idToken: tokens.id_token,
2706
+ accessToken: tokens.access_token
2707
+ };
2708
+ }
2520
2709
  var UsageError = class extends Error {
2521
2710
  };
2522
2711
  var HELP = `pidgeon id \u2014 a signed identity for your agents, on your own domain. No account.
@@ -2527,6 +2716,11 @@ var HELP = `pidgeon id \u2014 a signed identity for your agents, on your own dom
2527
2716
  list Agents on the domain
2528
2717
  revoke <name> Withdraw an agent's card now
2529
2718
  rotate Replace the domain's key; the old one verifies for another day
2719
+ grant <name> Grant <name> what it may do, as the domain
2720
+ --scope bookings.change --label "manage bookings"
2721
+ --subject customer:42 --audience delta.com --days 30
2722
+ revoke-grant <id> Take a grant back now
2723
+ sign-in <site> Sign in to a site as an agent --as <name> --grant <jws>
2530
2724
 
2531
2725
  --domain <domain> Which domain, when this machine holds more than one
2532
2726
  --json Print the API's answer as JSON
@@ -2569,7 +2763,7 @@ async function runId(argv, env = process.env, options = {}) {
2569
2763
  const [command, ...rest] = positional;
2570
2764
  if (!command || flags.has("help") || command === "help") return HELP;
2571
2765
  const dir = identityDir(env);
2572
- const json = flags.has("json");
2766
+ const json2 = flags.has("json");
2573
2767
  const say = options.stderr ?? (() => {
2574
2768
  });
2575
2769
  const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -2600,7 +2794,7 @@ async function runId(argv, env = process.env, options = {}) {
2600
2794
  made = true;
2601
2795
  }
2602
2796
  let state = await client2(identity).claim();
2603
- if (json && (state.status === "verified" || flags.has("no-wait")))
2797
+ if (json2 && (state.status === "verified" || flags.has("no-wait")))
2604
2798
  return JSON.stringify(state, null, 2);
2605
2799
  const lines = [
2606
2800
  made ? `Made a signing key for ${domain} on this machine (${identity.kid}).` : `Using the key for ${domain} on this machine.`
@@ -2629,13 +2823,13 @@ Or add them by hand:` : "Add these records at your DNS provider:",
2629
2823
  }
2630
2824
  lines.length = 0;
2631
2825
  }
2632
- if (json) return JSON.stringify(state, null, 2);
2826
+ if (json2) return JSON.stringify(state, null, 2);
2633
2827
  return [...lines, `${domain} is proved. Next: pidgeon id issue support-agent`].join("\n").trim();
2634
2828
  }
2635
2829
  case "check": {
2636
2830
  const identity = rest[0] ? await loadIdentity(rest[0], { dir, env }) : await which();
2637
2831
  const state = await client2(identity).check();
2638
- if (json) return JSON.stringify(state, null, 2);
2832
+ if (json2) return JSON.stringify(state, null, 2);
2639
2833
  return [
2640
2834
  `${state.domain}: ${state.status === "verified" ? "proved" : state.status}`,
2641
2835
  ...describeRecords(state)
@@ -2655,7 +2849,7 @@ Or add them by hand:` : "Add these records at your DNS provider:",
2655
2849
  ...kind ? { kind } : {},
2656
2850
  ...typeof onBehalfOf === "string" ? { on_behalf_of: onBehalfOf } : {}
2657
2851
  });
2658
- if (json) return JSON.stringify(card, null, 2);
2852
+ if (json2) return JSON.stringify(card, null, 2);
2659
2853
  return [
2660
2854
  `${card.card.address} has a card: ${card.url}`,
2661
2855
  `Found from the address at https://post.${identity.domain}/.well-known/post/card?address=${card.card.address}`,
@@ -2664,7 +2858,7 @@ Or add them by hand:` : "Add these records at your DNS provider:",
2664
2858
  }
2665
2859
  case "list": {
2666
2860
  const agents = await client2(await which()).list();
2667
- if (json) return JSON.stringify(agents, null, 2);
2861
+ if (json2) return JSON.stringify(agents, null, 2);
2668
2862
  if (agents.length === 0)
2669
2863
  return "No agents yet. `pidgeon id issue <name>` gives one a card.";
2670
2864
  return agents.map((agent) => `${agent.address} ${agent.kind} ${agent.card_url}`).join("\n");
@@ -2673,7 +2867,7 @@ Or add them by hand:` : "Add these records at your DNS provider:",
2673
2867
  const name = rest[0]?.trim().toLowerCase();
2674
2868
  if (!name) throw new UsageError("Name the agent: `pidgeon id revoke support-agent`.");
2675
2869
  const revoked = await client2(await which()).revoke(name);
2676
- if (json) return JSON.stringify(revoked, null, 2);
2870
+ if (json2) return JSON.stringify(revoked, null, 2);
2677
2871
  return `${revoked.address} has no card from now on. Copies already fetched lapse within a day.`;
2678
2872
  }
2679
2873
  case "rotate": {
@@ -2682,9 +2876,72 @@ Or add them by hand:` : "Add these records at your DNS provider:",
2682
2876
  const state = await client2(current).rotate(next);
2683
2877
  next.previous = { kid: current.kid, publicKey: current.publicKey };
2684
2878
  await saveIdentity(next, dir);
2685
- if (json) return JSON.stringify(state, null, 2);
2879
+ if (json2) return JSON.stringify(state, null, 2);
2686
2880
  return `${current.domain} signs with ${next.kid} now. ${current.kid} still verifies for a day.`;
2687
2881
  }
2882
+ case "grant": {
2883
+ const name = rest[0]?.trim().toLowerCase();
2884
+ const scope = flags.get("scope");
2885
+ const label = flags.get("label");
2886
+ const subject = flags.get("subject");
2887
+ if (!name || typeof scope !== "string" || typeof label !== "string") {
2888
+ throw new UsageError(
2889
+ 'Name the agent and the scope: `pidgeon id grant support-agent --scope bookings.change --label "manage bookings" --subject customer:42`.'
2890
+ );
2891
+ }
2892
+ const [type, ...idParts] = typeof subject === "string" ? subject.split(":") : [];
2893
+ if (!type || idParts.length === 0 || !["customer", "account", "order"].includes(type)) {
2894
+ throw new UsageError("--subject is customer:<id>, account:<id> or order:<id>.");
2895
+ }
2896
+ const days = flags.get("days");
2897
+ const audience = flags.get("audience");
2898
+ const grant = await client2(await which()).grant({
2899
+ agent: name,
2900
+ scope: { name: scope, label },
2901
+ subject: { type, id: idParts.join(":") },
2902
+ ...typeof audience === "string" ? { audience } : {},
2903
+ ...typeof days === "string" ? { expires_at: new Date(Date.now() + Number(days) * 864e5).toISOString() } : {}
2904
+ });
2905
+ if (json2) return JSON.stringify(grant, null, 2);
2906
+ return [
2907
+ `${grant.agent} may ${grant.scope.label} for ${grant.subject.type} ${grant.subject.id}${grant.audience ? ` at ${grant.audience}` : ""}, until ${grant.expires_at}.`,
2908
+ `Grant ${grant.id}. The agent carries it as Post-Grant; --json prints the JWS.`
2909
+ ].join("\n");
2910
+ }
2911
+ case "revoke-grant": {
2912
+ const grantId = rest[0]?.trim();
2913
+ if (!grantId) throw new UsageError("Name the grant: `pidgeon id revoke-grant <id>`.");
2914
+ const revoked = await client2(await which()).revokeGrant(grantId);
2915
+ if (json2) return JSON.stringify(revoked, null, 2);
2916
+ return `Grant ${revoked.id} is revoked. The next check of it fails.`;
2917
+ }
2918
+ case "sign-in": {
2919
+ const site = rest[0];
2920
+ const name = flags.get("as");
2921
+ if (!site || typeof name !== "string") {
2922
+ throw new UsageError(
2923
+ "Name the site and the agent: `pidgeon id sign-in https://site.example/api/auth/pidgeon/token --as support-agent`."
2924
+ );
2925
+ }
2926
+ const identity = await which();
2927
+ const card = await client2(identity).issue({ name: name.trim().toLowerCase() });
2928
+ const grant = flags.get("grant");
2929
+ try {
2930
+ const signedIn = await signInWithPidgeon(identity, {
2931
+ agent: card.card.address,
2932
+ site,
2933
+ card: card.jws,
2934
+ ...typeof grant === "string" ? { grant } : {},
2935
+ ...options.fetch ? { fetch: options.fetch } : {}
2936
+ });
2937
+ if (json2) return JSON.stringify(signedIn.site, null, 2);
2938
+ return `Signed in to ${new URL(site).host} as ${card.card.address}.
2939
+ ${JSON.stringify(signedIn.site, null, 2)}`;
2940
+ } catch (error) {
2941
+ if (error instanceof SignInError) throw new Error(`${error.step}: ${error.message}`);
2942
+ throw error;
2943
+ }
2944
+ }
2688
2945
  default:
2689
2946
  throw new UsageError(`No such command "${command}".
2690
2947
 
@@ -2890,4 +3147,4 @@ export {
2890
3147
  find,
2891
3148
  run
2892
3149
  };
2893
- //# sourceMappingURL=chunk-FA77SSPH.js.map
3150
+ //# sourceMappingURL=chunk-RWZQSI67.js.map