@fluid-app/fluid-cli-member-site 0.1.2 → 0.1.4

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
@@ -38,11 +38,20 @@ which it is with `--as member` or `--as admin`, and a site of the other owner
38
38
  kind is refused before any request, with the login it needs. Without `--as`,
39
39
  the server decides, and a refusal names the login the site needs.
40
40
 
41
- A member who is also an admin of the same company is refused by the member API
42
- and builds company sites only.
43
-
44
- `fluid login` signs in admins and reps only. A member signs in to this command
45
- with the member's own token, set in `FLUID_TOKEN`.
41
+ `fluid login` signs in admins and reps. An admin who is also a rep of the same
42
+ company builds both kinds of site after one normal `fluid login`: `--as admin`
43
+ builds company sites with the admin session, and `--as member` builds the
44
+ admin's own member sites with a rep session that the identity service narrows
45
+ the same login to, with no second sign-in. The identity service issues that
46
+ rep session only to a person who is a rep in the company. An admin who is not
47
+ one gets "You are not a rep at <company>, so you cannot build a member site
48
+ there. Use --as admin for company sites." before any member API request.
49
+
50
+ Only a login made through the identity service can be narrowed. A legacy login,
51
+ and a token set in `FLUID_TOKEN`, are sent as they are for `--as member` too,
52
+ so an admin on a legacy login is still refused by the member API and builds
53
+ company sites only. A member signs in to this command with the member's own
54
+ token, set in `FLUID_TOKEN`.
46
55
 
47
56
  ## Commands
48
57
 
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import chalk from "chalk";
3
- import { declareInputs, exitOnInputsError, findProjectConfig, getAuthToken, readConfig, withInternalReadsAsync } from "@fluid-app/fluid-cli";
3
+ import { declareInputs, exitOnInputsError, findProjectConfig, getAuthToken, getRoleAuthTokenAsync, isRoleNotGrantedError, readConfig, withInternalReadsAsync } from "@fluid-app/fluid-cli";
4
4
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
5
5
  import http from "node:http";
6
6
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
@@ -555,6 +555,24 @@ function resolveAuthToken() {
555
555
  return getAuthToken() ?? null;
556
556
  }
557
557
  /**
558
+ * The stored login's token narrowed to a rep, which is what the member API
559
+ * accepts from a person who is also an admin: the identity service issues it
560
+ * only for a person who holds rep standing in the company. A login that
561
+ * cannot be narrowed, a literal token (`FLUID_TOKEN`) or a legacy profile,
562
+ * gives the token `resolveAuthToken` gives.
563
+ *
564
+ * Throws a MemberSiteCliError, before any member API request, when the person
565
+ * is not a rep in the company.
566
+ */
567
+ async function resolveRepToken() {
568
+ try {
569
+ return await getRoleAuthTokenAsync(["rep"]) ?? null;
570
+ } catch (error) {
571
+ if (!isRoleNotGrantedError(error)) throw error;
572
+ throw new MemberSiteCliError(`You are not a rep at ${error.companyName ?? "this company"}, so you cannot build a member site there. Use --as admin for company sites.`);
573
+ }
574
+ }
575
+ /**
558
576
  * A client for one login. Reads that fail with 502, 503 or 504 are retried
559
577
  * twice, as the theme CLI retries them. A write is never retried, because a
560
578
  * failed response may follow a successful change, and a 404 never is,
@@ -604,9 +622,18 @@ function loginFor(ownerKind) {
604
622
  function describeLogin(kind) {
605
623
  return kind === "member" ? "a member login" : "an admin login";
606
624
  }
607
- /** The login a command acts with, or an error that says to sign in. */
608
- function loadLogin(kind, resolveToken = resolveAuthToken) {
609
- const token = resolveToken();
625
+ const defaultSources = {
626
+ token: resolveAuthToken,
627
+ repToken: resolveRepToken
628
+ };
629
+ /**
630
+ * The login a command acts with, or an error that says to sign in. With
631
+ * `--as member` the token is the stored login's rep session, so an admin who
632
+ * is also a rep builds their own sites with the login they already have;
633
+ * otherwise it is the stored login's token as it is.
634
+ */
635
+ async function loadLogin(kind, sources = defaultSources) {
636
+ const token = kind === "member" ? await sources.repToken() : sources.token();
610
637
  if (!token) throw new MemberSiteCliError("Not logged in. Run `fluid login` first (or set FLUID_TOKEN).");
611
638
  return {
612
639
  token,
@@ -749,7 +776,7 @@ async function runCreate(handle, options, deps = defaultDeps$5) {
749
776
  assertValidHandle(handle);
750
777
  const name = options.name?.trim() || nameFromHandle(handle);
751
778
  const ownerKind = ownerKindOf(options);
752
- const login = deps.loadLogin(parseLoginKind(options.as));
779
+ const login = await deps.loadLogin(parseLoginKind(options.as));
753
780
  const site = await clientForOwner(ownerKind, login, deps.createClients(login.token)).createSite({
754
781
  name,
755
782
  handle
@@ -1015,7 +1042,7 @@ var PreviewProxy = class {
1015
1042
  return fetch(`${this.previewUrl}${path}`, {
1016
1043
  method: "GET",
1017
1044
  headers: {
1018
- authorization: `Bearer ${this.options.getToken()}`,
1045
+ authorization: `Bearer ${await this.options.getToken()}`,
1019
1046
  accept: route.kind === "page" ? "text/html" : "*/*",
1020
1047
  "accept-encoding": "identity",
1021
1048
  "user-agent": "Fluid CLI"
@@ -1359,7 +1386,7 @@ async function openBoundSite(dirArg, as, deps, context) {
1359
1386
  if (!stored) throw new MemberSiteCliError(`${dir} is not a pulled site: it has no readable ${BINDING_FILE}. Pull the site first: fluid member-site pull <site> [dir]`);
1360
1387
  const binding = structuredClone(stored);
1361
1388
  context.ownerKind = binding.owner_kind;
1362
- const login = deps.loadLogin(parseLoginKind(as));
1389
+ const login = await deps.loadLogin(parseLoginKind(as));
1363
1390
  const api = clientForOwner(binding.owner_kind, login, deps.createClients(login.token));
1364
1391
  if (binding.api_base !== getApiBase()) throw new MemberSiteCliError(`${dir} was pulled from ${binding.api_base}, and this login is for ${getApiBase()}. Use the login it was pulled with, or pull the site again.`);
1365
1392
  return {
@@ -2717,7 +2744,7 @@ async function runDev(dirArg, options, deps = defaultDeps$2, context = {}) {
2717
2744
  ownerKind: binding.owner_kind
2718
2745
  },
2719
2746
  apiBase,
2720
- getToken: () => deps.loadLogin(parseLoginKind(options.as)).token
2747
+ getToken: async () => (await deps.loadLogin(parseLoginKind(options.as))).token
2721
2748
  });
2722
2749
  if (await proxy.probe() === "unavailable") throw new MemberSiteCliError(DRAFT_PREVIEW_UNAVAILABLE);
2723
2750
  await checkPortAvailable(DEV_HOST, port);
@@ -4972,7 +4999,7 @@ const defaultDeps$1 = {
4972
4999
  log: (line) => console.log(line)
4973
5000
  };
4974
5001
  async function runList(options, deps = defaultDeps$1) {
4975
- const login = deps.loadLogin(parseLoginKind(options.as));
5002
+ const login = await deps.loadLogin(parseLoginKind(options.as));
4976
5003
  const listing = await listForLogin(login, deps.createClients(login.token));
4977
5004
  for (const line of formatListing(listing)) deps.log(line);
4978
5005
  }
@@ -5050,7 +5077,7 @@ function formatPull(result) {
5050
5077
  * reported with the login the site needs.
5051
5078
  */
5052
5079
  async function runPull(ref, dirArg, options, deps = defaultDeps, context = {}) {
5053
- const login = deps.loadLogin(parseLoginKind(options.as));
5080
+ const login = await deps.loadLogin(parseLoginKind(options.as));
5054
5081
  const clients = deps.createClients(login.token);
5055
5082
  const known = await readBinding(resolve(dirArg ?? ref));
5056
5083
  if (known && (known.site_id === ref || known.handle === ref)) {