makeDecodeFromCapData = (decodeOptions = {}) => {
  const {
    decodeRemotableFromCapData = dontDecodeRemotableOrPromiseFromCapData,
    decodePromiseFromCapData = dontDecodeRemotableOrPromiseFromCapData,
    decodeErrorFromCapData = dontDecodeErrorFromCapData,
  } = decodeOptions;

  decodeRemotableFromCapData === decodePromiseFromCapData ||
    Fail`An implementation restriction for now: If either decodeRemotableFromCapData or decodePromiseFromCapData is provided, both must be provided and they must be the same: ${q(
      decodeRemotableFromCapData,
    )} vs ${q(decodePromiseFromCapData)}`;

  /**
   * `decodeFromCapData` may rely on `jsonEncoded` being the result of a
   * plain call to JSON.parse. However, it *cannot* rely on `jsonEncoded`
   * having been produced by JSON.stringify on the output of `encodeToCapData`
   * above, i.e., `decodeFromCapData` cannot rely on `jsonEncoded` being a
   * valid marshalled representation. Rather, `decodeFromCapData` must
   * validate that.
   *
   * @param {Encoding} jsonEncoded must be hardened
   */
  const decodeFromCapData = jsonEncoded => {
    if (!isObject(jsonEncoded)) {
      // primitives pass through
      return jsonEncoded;
    }
    if (isArray(jsonEncoded)) {
      return jsonEncoded.map(encodedVal => decodeFromCapData(encodedVal));
    } else if (hasQClass(jsonEncoded)) {
      const qclass = jsonEncoded[QCLASS];
      typeof qclass === 'string' ||
        Fail`invalid ${q(QCLASS)} typeof ${q(typeof qclass)}`;
      switch (qclass) {
        // Encoding of primitives not handled by JSON
        case 'undefined': {
          return undefined;
        }
        case 'NaN': {
          return NaN;
        }
        case 'Infinity': {
          return Infinity;
        }
        case '-Infinity': {
          return -Infinity;
        }
        case 'bigint': {
          const { digits } = jsonEncoded;
          typeof digits === 'string' ||
            Fail`invalid digits typeof ${q(typeof digits)}`;
          return BigInt(digits);
        }
        case '@@asyncIterator': {
          // Deprecated qclass. TODO make conditional
          // on environment variable. Eventually remove, but after confident
          // that there are no more supported senders.
          //
          return Symbol.asyncIterator;
        }
        case 'symbol': {
          const { name } = jsonEncoded;
          return passableSymbolForName(name);
        }
        case 'tagged': {
          const { tag, payload } = jsonEncoded;
          return makeTagged(tag, decodeFromCapData(payload));
        }
        case 'slot': {
          // See note above about how the current encoding cannot reliably
          // distinguish which we should call, so in the non-default case
          // both must be the same and it doesn't matter which we call.
          const decoded = decodeRemotableFromCapData(
            jsonEncoded,
            decodeFromCapData,
          );
          // BEWARE: capdata does not check that `decoded` is
          // a promise or a remotable, since that would break some
          // capdata clients. We are deprecating capdata, and these clients
          // will need to update before switching to smallcaps.
          return decoded;
        }
        case 'error': {
          const decoded = decodeErrorFromCapData(
            jsonEncoded,
            decodeFromCapData,
          );
          if (passStyleOf(decoded) === 'error') {
            return decoded;
          }
          throw Fail`internal: decodeErrorFromCapData option must return an error: ${decoded}`;
        }
        case 'hilbert': {
          const { original, rest } = jsonEncoded;
          hasOwnPropertyOf(jsonEncoded, 'original') ||
            Fail`Invalid Hilbert Hotel encoding ${jsonEncoded}`;
          // Don't harden since we're not done mutating it
          const result = { [QCLASS]: decodeFromCapData(original) };
          if (hasOwnPropertyOf(jsonEncoded, 'rest')) {
            const isNonEmptyObject =
              typeof rest === 'object' &&
              rest !== null &&
              ownKeys(rest).length >= 1;
            if (!isNonEmptyObject) {
              throw Fail`Rest encoding must be a non-empty object: ${rest}`;
            }
            const restObj = decodeFromCapData(rest);
            // TODO really should assert that `passStyleOf(rest)` is
            // `'copyRecord'` but we'd have to harden it and it is too
            // early to do that.
            !hasOwnPropertyOf(restObj, QCLASS) ||
              Fail`Rest must not contain its own definition of ${q(QCLASS)}`;
            defineProperties(result, getOwnPropertyDescriptors(restObj));
          }
          return result;
        }
        // @ts-expect-error This is the error case we're testing for
        case 'ibid': {
          throw Fail`The capData protocol no longer supports ${q(QCLASS)} ${q(
            qclass,
          )}`;
        }
        default: {
          throw assert.fail(
            X`unrecognized ${q(QCLASS)} ${q(qclass)}`,
            TypeError,
          );
        }
      }
    } else {
      assert(typeof jsonEncoded === 'object' && jsonEncoded !== null);
      const decodeEntry = ([name, encodedVal]) => {
        typeof name === 'string' ||
          Fail`Property ${q(name)} of ${jsonEncoded} must be a string`;
        return [name, decodeFromCapData(encodedVal)];
      };
      const decodedEntries = entries(jsonEncoded).map(decodeEntry);
      return fromEntries(decodedEntries);
    }
  };
  return harden(decodeFromCapData);
}
The current encoding does not give the decoder enough into to distinguish
whether a slot represents a promise or a remotable. As an implementation
restriction until this is fixed, if either is provided, both must be
provided and they must be the same.

This seems like the best starting point to incrementally evolve to an
API where these can reliably differ.
See https://github.com/Agoric/agoric-sdk/issues/4334

@param {DecodeOptions} [decodeOptions]
@returns {(encoded: Encoding) => Passable}